1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4#[serde(deny_unknown_fields)]
5pub struct ActiveCircleEpochCore {
6 pub epoch_id: CircleEpochId,
7 pub key_fingerprint: KeyFingerprint,
8 pub owners: Vec<String>,
9 pub access_root: ObjectHash,
10 pub origin: CircleEpochOrigin,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case", deny_unknown_fields)]
15pub enum CircleEpochOrigin {
16 Founder,
17 Closed {
18 closed_epoch_id: CircleEpochId,
19 close_control: CircleControlCoord,
20 close_id: CircleEpochCloseId,
21 outcome_hash: ObjectHash,
22 cutoff: CommitFrontier,
23 },
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct MergeActiveCircleEpoch {
29 pub common: ActiveCircleEpochCore,
30 pub metadata: MergeCircleMetadataStateRef,
31 pub roster: MergeCircleRosterStateRef,
32 pub store_membership: StoreMembershipStateRef,
33 pub covered_control_heads: Vec<MergeCircleControlHeadRef>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct CircleEpochCloseParticipant {
39 pub registration: StoreDeviceRegistrationRef,
40 pub response_slot: ObjectSlot,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct CircleEpochCloseIntentBody {
48 pub store_root_hash: ObjectHash,
49 pub circle_id: CircleId,
50 pub close_id: CircleEpochCloseId,
51 pub epoch_id: CircleEpochId,
52 pub predecessor_roster: MergeCircleRosterStateRef,
53 pub removal: CircleRosterEntry,
54 pub remaining_roster_state_hash: ObjectHash,
55 pub owner_pubkey: String,
56}
57
58impl SignedBody for CircleEpochCloseIntentBody {
59 const DOMAIN: &'static [u8] = CLOSE_INTENT_DOMAIN;
60}
61
62pub type CircleEpochCloseIntent = Signed<CircleEpochCloseIntentBody>;
63
64impl CircleEpochCloseIntentBody {
65 pub(super) fn verify_shape(&self) -> bool {
66 self.removal.verify()
67 && self.removal.store_root_hash == self.store_root_hash
68 && self.removal.circle_id == self.circle_id
69 && self.removal.author_pubkey == self.owner_pubkey
70 && matches!(
71 self.removal.change,
72 crate::circle_roster::CircleRosterChange::RemoveMember { .. }
73 )
74 }
75}
76
77impl CircleEpochCloseIntent {
78 #[allow(clippy::too_many_arguments)]
79 pub fn signed(
80 store_root_hash: ObjectHash,
81 circle_id: CircleId,
82 close_id: CircleEpochCloseId,
83 epoch_id: CircleEpochId,
84 predecessor_roster: MergeCircleRosterStateRef,
85 removal: CircleRosterEntry,
86 remaining_roster_state_hash: ObjectHash,
87 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
88 ) -> Result<Self, CircleTransitionError> {
89 let body = CircleEpochCloseIntentBody {
90 store_root_hash,
91 circle_id,
92 close_id,
93 epoch_id,
94 predecessor_roster,
95 removal,
96 remaining_roster_state_hash,
97 owner_pubkey: keys::public_key_hex(signer),
98 };
99 if !body.verify_shape() {
100 return Err(CircleTransitionError::InvalidCurrentState);
101 }
102 Ok(Signed::sign(body, signer))
103 }
104
105 pub fn verify(&self) -> bool {
106 self.body().verify_shape() && self.verify_by(&self.owner_pubkey).is_ok()
107 }
108
109 pub fn intent_hash(&self) -> ObjectHash {
110 self.hash()
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
115#[serde(deny_unknown_fields)]
116pub struct CircleEpochCloseIntentRef {
117 pub close_id: CircleEpochCloseId,
118 pub intent_hash: ObjectHash,
119 pub object: ExactObjectRef,
120}
121
122impl CircleEpochCloseIntentRef {
123 pub fn from_intent(
124 intent: &CircleEpochCloseIntent,
125 object: ExactObjectRef,
126 ) -> Result<Self, CircleTransitionError> {
127 let reference = Self {
128 close_id: intent.close_id,
129 intent_hash: intent.intent_hash(),
130 object,
131 };
132 if reference.object.slot().logical_key()
133 != format!(
134 "{}.json",
135 circle_epoch_close_intent_semantic_prefix(
136 intent.circle_id,
137 intent.close_id,
138 intent.intent_hash(),
139 )
140 )
141 {
142 return Err(CircleTransitionError::InvalidCurrentState);
143 }
144 Ok(reference)
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(deny_unknown_fields)]
150pub struct CircleEpochClose {
151 pub close_id: CircleEpochCloseId,
152 pub frozen_epoch: MergeActiveCircleEpoch,
153 pub intent: CircleEpochCloseIntentRef,
154 pub frozen_device_state: StoreDeviceStateRef,
155 pub participants: Vec<CircleEpochCloseParticipant>,
156 pub provisional_frontier: CommitFrontier,
157 pub outcome_slot: ObjectSlot,
158}
159
160impl CircleEpochClose {
161 pub(super) fn verify_shape(&self, circle_id: CircleId) -> bool {
162 crate::store_commit::validate_commit_frontier(&self.provisional_frontier).is_ok()
163 && self.intent.close_id == self.close_id
164 && !self.participants.is_empty()
165 && self
166 .participants
167 .windows(2)
168 .all(|pair| pair[0].registration.device_id < pair[1].registration.device_id)
169 && self.participants.iter().all(|participant| {
170 participant.response_slot.logical_key()
171 == format!(
172 "{}.json",
173 circle_epoch_close_response_semantic_prefix(
174 circle_id,
175 self.close_id,
176 participant.registration.device_id,
177 )
178 )
179 })
180 && self.outcome_slot.logical_key()
181 == format!(
182 "{}.json",
183 circle_epoch_close_outcome_semantic_prefix(circle_id, self.close_id)
184 )
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191#[serde(deny_unknown_fields)]
192pub struct CircleEpochCloseResponseBody {
193 pub store_root_hash: ObjectHash,
194 pub circle_id: CircleId,
195 pub close_id: CircleEpochCloseId,
196 pub close_control: CircleControlCoord,
197 pub registration: StoreDeviceRegistrationRef,
198 pub frontier: CommitFrontier,
199}
200
201impl SignedBody for CircleEpochCloseResponseBody {
202 const DOMAIN: &'static [u8] = CLOSE_RESPONSE_DOMAIN;
203}
204
205pub type CircleEpochCloseResponse = Signed<CircleEpochCloseResponseBody>;
206
207impl CircleEpochCloseResponse {
208 pub fn signed(
209 control: &PreparedCircleControl,
210 registration: StoreDeviceRegistrationRef,
211 frontier: CommitFrontier,
212 author: &StoreDeviceRegistration,
213 signer: &UserKeypair,
214 ) -> Result<Self, CircleTransitionError> {
215 let CircleControlState::EpochClose(close) = control.value.state() else {
216 return Err(CircleTransitionError::InvalidCurrentState);
217 };
218 let response = Signed::sign(
219 CircleEpochCloseResponseBody {
220 store_root_hash: control.value.store_root_hash,
221 circle_id: control.value.circle_id,
222 close_id: close.close_id,
223 close_control: control.coord.clone(),
224 registration,
225 frontier,
226 },
227 signer,
228 );
229 if !response.verify_for(control, author) {
230 return Err(CircleTransitionError::InvalidCurrentState);
231 }
232 Ok(response)
233 }
234
235 pub fn verify_for(
236 &self,
237 control: &PreparedCircleControl,
238 author: &StoreDeviceRegistration,
239 ) -> bool {
240 let CircleControlState::EpochClose(close) = control.value.state() else {
241 return false;
242 };
243 control.verify()
244 && self.store_root_hash == control.value.store_root_hash
245 && self.circle_id == control.value.circle_id
246 && self.close_id == close.close_id
247 && self.close_control == control.coord
248 && crate::store_commit::validate_commit_frontier(&self.frontier).is_ok()
249 && self.frontier.covers(&close.provisional_frontier)
250 && self.registration.verify_registration(author).is_ok()
251 && author.store_root.store_root_hash == self.store_root_hash
252 && close
253 .participants
254 .iter()
255 .any(|participant| participant.registration == self.registration)
256 && self.verify_by(&author.device_signing_pubkey).is_ok()
257 }
258
259 pub(crate) fn response_hash(&self) -> ObjectHash {
260 self.hash()
261 }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
265#[serde(deny_unknown_fields)]
266pub struct CircleEpochCloseResponseRef {
267 pub registration: StoreDeviceRegistrationRef,
268 pub frontier: CommitFrontier,
269 pub response_hash: ObjectHash,
270 pub object: ExactObjectRef,
271}
272
273impl CircleEpochCloseResponseRef {
274 pub fn from_response(
275 response: &CircleEpochCloseResponse,
276 object: ExactObjectRef,
277 ) -> Result<Self, CircleTransitionError> {
278 if object.slot().logical_key()
279 != format!(
280 "{}.json",
281 circle_epoch_close_response_semantic_prefix(
282 response.circle_id,
283 response.close_id,
284 response.registration.device_id,
285 )
286 )
287 {
288 return Err(CircleTransitionError::InvalidCurrentState);
289 }
290 Ok(Self {
291 registration: response.registration.clone(),
292 frontier: response.frontier.clone(),
293 response_hash: response.response_hash(),
294 object,
295 })
296 }
297
298 pub(crate) fn verify_response(&self, response: &CircleEpochCloseResponse) -> bool {
299 self.registration == response.registration
300 && self.frontier == response.frontier
301 && self.response_hash == response.response_hash()
302 }
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311#[serde(deny_unknown_fields)]
312pub struct CircleEpochCloseExclusionBody {
313 pub store_root_hash: ObjectHash,
314 pub circle_id: CircleId,
315 pub close_id: CircleEpochCloseId,
316 pub close_control: CircleControlCoord,
317 pub excluded: StoreDeviceRegistrationRef,
318 pub owner_pubkey: String,
319}
320
321impl SignedBody for CircleEpochCloseExclusionBody {
322 const DOMAIN: &'static [u8] = CLOSE_EXCLUSION_DOMAIN;
323}
324
325pub type CircleEpochCloseExclusion = Signed<CircleEpochCloseExclusionBody>;
326
327impl CircleEpochCloseExclusion {
328 pub fn signed(
329 control: &PreparedCircleControl,
330 excluded: StoreDeviceRegistrationRef,
331 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
332 ) -> Result<Self, CircleTransitionError> {
333 let CircleControlState::EpochClose(close) = control.value.state() else {
334 return Err(CircleTransitionError::InvalidCurrentState);
335 };
336 let exclusion = Signed::sign(
337 CircleEpochCloseExclusionBody {
338 store_root_hash: control.value.store_root_hash,
339 circle_id: control.value.circle_id,
340 close_id: close.close_id,
341 close_control: control.coord.clone(),
342 excluded,
343 owner_pubkey: keys::public_key_hex(signer),
344 },
345 signer,
346 );
347 if !exclusion.verify_shape(control) {
348 return Err(CircleTransitionError::InvalidCurrentState);
349 }
350 Ok(exclusion)
351 }
352
353 pub(super) fn verify_shape(&self, control: &PreparedCircleControl) -> bool {
354 let CircleControlState::EpochClose(close) = control.value.state() else {
355 return false;
356 };
357 control.verify()
358 && self.store_root_hash == control.value.store_root_hash
359 && self.circle_id == control.value.circle_id
360 && self.close_id == close.close_id
361 && self.close_control == control.coord
362 && close
363 .participants
364 .iter()
365 .any(|participant| participant.registration == self.excluded)
366 && close
367 .frozen_epoch
368 .common
369 .owners
370 .contains(&self.owner_pubkey)
371 }
372
373 pub fn verify_for(&self, control: &PreparedCircleControl) -> bool {
374 self.verify_shape(control) && self.verify_by(&self.owner_pubkey).is_ok()
375 }
376
377 pub(crate) fn exclusion_hash(&self) -> ObjectHash {
378 self.hash()
379 }
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
383#[serde(deny_unknown_fields)]
384pub struct CircleEpochCloseExclusionRef {
385 pub registration: StoreDeviceRegistrationRef,
386 pub exclusion_hash: ObjectHash,
387 pub object: ExactObjectRef,
388}
389
390impl CircleEpochCloseExclusionRef {
391 pub fn from_exclusion(
392 exclusion: &CircleEpochCloseExclusion,
393 object: ExactObjectRef,
394 ) -> Result<Self, CircleTransitionError> {
395 if object.slot().logical_key()
396 != format!(
397 "{}.json",
398 circle_epoch_close_response_semantic_prefix(
399 exclusion.circle_id,
400 exclusion.close_id,
401 exclusion.excluded.device_id,
402 )
403 )
404 {
405 return Err(CircleTransitionError::InvalidCurrentState);
406 }
407 Ok(Self {
408 registration: exclusion.excluded.clone(),
409 exclusion_hash: exclusion.exclusion_hash(),
410 object,
411 })
412 }
413
414 pub(crate) fn verify_exclusion(&self, exclusion: &CircleEpochCloseExclusion) -> bool {
415 self.registration == exclusion.excluded && self.exclusion_hash == exclusion.exclusion_hash()
416 }
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
422#[serde(rename_all = "snake_case", deny_unknown_fields)]
423pub enum CircleEpochCloseResponseSlotValue {
424 Response(CircleEpochCloseResponse),
425 Exclusion(CircleEpochCloseExclusion),
426}
427
428impl CircleEpochCloseResponseSlotValue {
429 pub fn to_bytes(&self) -> Vec<u8> {
430 serde_json::to_vec(self)
431 .expect("Circle epoch-close response slot value serialization cannot fail")
432 }
433
434 pub fn parse(bytes: &[u8]) -> Result<Self, CircleTransitionError> {
435 let value: Self = serde_json::from_slice(bytes)
436 .map_err(|_| CircleTransitionError::InvalidCurrentState)?;
437 if value.to_bytes() != bytes {
438 return Err(CircleTransitionError::InvalidCurrentState);
439 }
440 Ok(value)
441 }
442}
443
444#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
448#[serde(rename_all = "snake_case", deny_unknown_fields)]
449pub enum CircleEpochCloseSettlement {
450 Response(CircleEpochCloseResponseRef),
451 Exclusion(CircleEpochCloseExclusionRef),
452}
453
454impl CircleEpochCloseSettlement {
455 pub fn registration(&self) -> &StoreDeviceRegistrationRef {
456 match self {
457 Self::Response(reference) => &reference.registration,
458 Self::Exclusion(reference) => &reference.registration,
459 }
460 }
461
462 pub fn object(&self) -> &ExactObjectRef {
463 match self {
464 Self::Response(reference) => &reference.object,
465 Self::Exclusion(reference) => &reference.object,
466 }
467 }
468
469 pub fn response_frontier(&self) -> Option<&CommitFrontier> {
470 match self {
471 Self::Response(reference) => Some(&reference.frontier),
472 Self::Exclusion(_) => None,
473 }
474 }
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
478#[serde(deny_unknown_fields)]
479pub struct CircleEpochSuccessor {
480 pub epoch_id: CircleEpochId,
481 pub key_fingerprint: KeyFingerprint,
482 pub owners: Vec<String>,
483 pub access_root: ObjectHash,
484 pub metadata: MergeCircleMetadataStateRef,
485 pub roster: MergeCircleRosterStateRef,
486 pub store_membership: StoreMembershipStateRef,
487}
488
489#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
491#[serde(deny_unknown_fields)]
492pub struct CircleEpochCloseOutcomeBody {
493 pub store_root_hash: ObjectHash,
494 pub circle_id: CircleId,
495 pub close_id: CircleEpochCloseId,
496 pub close_control: CircleControlCoord,
497 pub intent: CircleEpochCloseIntentRef,
498 pub responses: Vec<CircleEpochCloseSettlement>,
499 pub cutoff: CommitFrontier,
500 pub successor: CircleEpochSuccessor,
501 pub owner_pubkey: String,
502}
503
504impl SignedBody for CircleEpochCloseOutcomeBody {
505 const DOMAIN: &'static [u8] = CLOSE_OUTCOME_DOMAIN;
506}
507
508pub type CircleEpochCloseOutcome = Signed<CircleEpochCloseOutcomeBody>;
509
510impl CircleEpochCloseOutcome {
511 pub fn signed(
512 control: &PreparedCircleControl,
513 intent: &CircleEpochCloseIntent,
514 responses: Vec<CircleEpochCloseSettlement>,
515 successor: CircleEpochSuccessor,
516 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
517 ) -> Result<Self, CircleTransitionError> {
518 let CircleControlState::EpochClose(close) = control.value.state() else {
519 return Err(CircleTransitionError::InvalidCurrentState);
520 };
521 let cutoff = responses
522 .iter()
523 .filter_map(CircleEpochCloseSettlement::response_frontier)
524 .try_fold(close.provisional_frontier.clone(), |cutoff, frontier| {
525 cutoff.join(frontier.clone())
526 })
527 .map_err(|_| CircleTransitionError::InvalidCurrentState)?;
528 let outcome = Signed::sign(
529 CircleEpochCloseOutcomeBody {
530 store_root_hash: control.value.store_root_hash,
531 circle_id: control.value.circle_id,
532 close_id: close.close_id,
533 close_control: control.coord.clone(),
534 intent: close.intent.clone(),
535 responses,
536 cutoff,
537 successor,
538 owner_pubkey: keys::public_key_hex(signer),
539 },
540 signer,
541 );
542 if !outcome.verify_shape(control) || !outcome.verify_intent(intent) {
543 return Err(CircleTransitionError::InvalidCurrentState);
544 }
545 Ok(outcome)
546 }
547
548 pub(super) fn verify_shape(&self, control: &PreparedCircleControl) -> bool {
549 let CircleControlState::EpochClose(close) = control.value.state() else {
550 return false;
551 };
552 let responses_are_canonical = self
553 .responses
554 .windows(2)
555 .all(|pair| pair[0].registration().device_id < pair[1].registration().device_id);
556 let responses_match_participants =
557 self.responses.len() == close.participants.len()
558 && self.responses.iter().zip(&close.participants).all(
559 |(settlement, participant)| {
560 settlement.registration() == &participant.registration
561 && settlement.object().slot() == &participant.response_slot
562 && settlement
563 .response_frontier()
564 .is_none_or(|frontier| frontier.covers(&close.provisional_frontier))
565 },
566 );
567 let expected_cutoff = self
568 .responses
569 .iter()
570 .filter_map(CircleEpochCloseSettlement::response_frontier)
571 .try_fold(close.provisional_frontier.clone(), |cutoff, frontier| {
572 cutoff.join(frontier.clone())
573 });
574 control.verify()
575 && self.store_root_hash == control.value.store_root_hash
576 && self.circle_id == control.value.circle_id
577 && self.close_id == close.close_id
578 && self.close_control == control.coord
579 && self.intent == close.intent
580 && responses_are_canonical
581 && responses_match_participants
582 && expected_cutoff.is_ok_and(|cutoff| cutoff == self.cutoff)
583 && crate::store_commit::validate_commit_frontier(&self.cutoff).is_ok()
584 && self.successor.epoch_id != close.frozen_epoch.common.epoch_id
585 && self.successor.key_fingerprint != close.frozen_epoch.common.key_fingerprint
586 && !self.successor.owners.is_empty()
587 && self
588 .successor
589 .owners
590 .windows(2)
591 .all(|pair| pair[0] < pair[1])
592 && close
593 .frozen_epoch
594 .common
595 .owners
596 .contains(&self.owner_pubkey)
597 }
598
599 fn verify_intent(&self, intent: &CircleEpochCloseIntent) -> bool {
600 intent.verify()
601 && intent.close_id == self.close_id
602 && intent.circle_id == self.circle_id
603 && intent.store_root_hash == self.store_root_hash
604 && intent.intent_hash() == self.intent.intent_hash
605 && self.successor.roster.state_hash == intent.remaining_roster_state_hash
606 }
607
608 pub fn verify_for(
609 &self,
610 control: &PreparedCircleControl,
611 intent: &CircleEpochCloseIntent,
612 settlements: &[(
613 CircleEpochCloseSettlement,
614 CircleEpochCloseResponseSlotValue,
615 )],
616 ) -> bool {
617 self.verify_shape(control)
618 && self.verify_intent(intent)
619 && self.responses.len() == settlements.len()
620 && self
621 .responses
622 .iter()
623 .zip(settlements)
624 .all(|(expected, (settlement, slot_value))| {
625 expected == settlement
626 && match (settlement, slot_value) {
627 (
628 CircleEpochCloseSettlement::Response(reference),
629 CircleEpochCloseResponseSlotValue::Response(response),
630 ) => {
631 reference.verify_response(response)
632 && response.close_control == self.close_control
633 }
634 (
635 CircleEpochCloseSettlement::Exclusion(reference),
636 CircleEpochCloseResponseSlotValue::Exclusion(exclusion),
637 ) => {
638 reference.verify_exclusion(exclusion)
639 && exclusion.verify_for(control)
640 && exclusion.close_control == self.close_control
641 }
642 _ => false,
643 }
644 })
645 && self.verify_signature()
646 }
647
648 pub fn verify_signature(&self) -> bool {
649 self.verify_by(&self.owner_pubkey).is_ok()
650 }
651
652 pub fn outcome_hash(&self) -> ObjectHash {
653 self.hash()
654 }
655}
656
657#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
658#[serde(deny_unknown_fields)]
659pub struct CircleEpochCloseOutcomeRef {
660 pub close_id: CircleEpochCloseId,
661 pub outcome_hash: ObjectHash,
662 pub object: ExactObjectRef,
663}
664
665impl CircleEpochCloseOutcomeRef {
666 pub fn from_outcome(
667 outcome: &CircleEpochCloseOutcome,
668 object: ExactObjectRef,
669 ) -> Result<Self, CircleTransitionError> {
670 if object.slot().logical_key()
671 != format!(
672 "{}.json",
673 circle_epoch_close_outcome_semantic_prefix(outcome.circle_id, outcome.close_id)
674 )
675 {
676 return Err(CircleTransitionError::InvalidCurrentState);
677 }
678 Ok(Self {
679 close_id: outcome.close_id,
680 outcome_hash: outcome.outcome_hash(),
681 object,
682 })
683 }
684}
685
686#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
692#[serde(deny_unknown_fields)]
693pub struct CircleEpochCloseCancellationBody {
694 pub store_root_hash: ObjectHash,
695 pub circle_id: CircleId,
696 pub close_id: CircleEpochCloseId,
697 pub close_control: CircleControlCoord,
698 pub intent: CircleEpochCloseIntentRef,
699 pub owner_pubkey: String,
700}
701
702impl SignedBody for CircleEpochCloseCancellationBody {
703 const DOMAIN: &'static [u8] = CLOSE_CANCELLATION_DOMAIN;
704}
705
706pub type CircleEpochCloseCancellation = Signed<CircleEpochCloseCancellationBody>;
707
708impl CircleEpochCloseCancellation {
709 pub fn signed(
710 control: &PreparedCircleControl,
711 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
712 ) -> Result<Self, CircleTransitionError> {
713 let CircleControlState::EpochClose(close) = control.value.state() else {
714 return Err(CircleTransitionError::InvalidCurrentState);
715 };
716 let cancellation = Signed::sign(
717 CircleEpochCloseCancellationBody {
718 store_root_hash: control.value.store_root_hash,
719 circle_id: control.value.circle_id,
720 close_id: close.close_id,
721 close_control: control.coord.clone(),
722 intent: close.intent.clone(),
723 owner_pubkey: keys::public_key_hex(signer),
724 },
725 signer,
726 );
727 if !cancellation.verify_shape(control) {
728 return Err(CircleTransitionError::InvalidCurrentState);
729 }
730 Ok(cancellation)
731 }
732
733 pub(super) fn verify_shape(&self, control: &PreparedCircleControl) -> bool {
734 let CircleControlState::EpochClose(close) = control.value.state() else {
735 return false;
736 };
737 control.verify()
738 && self.store_root_hash == control.value.store_root_hash
739 && self.circle_id == control.value.circle_id
740 && self.close_id == close.close_id
741 && self.close_control == control.coord
742 && self.intent == close.intent
743 && close
744 .frozen_epoch
745 .common
746 .owners
747 .contains(&self.owner_pubkey)
748 }
749
750 pub fn verify_for(&self, control: &PreparedCircleControl) -> bool {
751 self.verify_shape(control) && self.verify_signature()
752 }
753
754 pub fn verify_signature(&self) -> bool {
755 self.verify_by(&self.owner_pubkey).is_ok()
756 }
757
758 pub fn cancellation_hash(&self) -> ObjectHash {
759 self.hash()
760 }
761}
762
763#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
764#[serde(deny_unknown_fields)]
765pub struct CircleEpochCloseCancellationRef {
766 pub close_id: CircleEpochCloseId,
767 pub cancellation_hash: ObjectHash,
768 pub object: ExactObjectRef,
769}
770
771impl CircleEpochCloseCancellationRef {
772 pub fn from_cancellation(
773 cancellation: &CircleEpochCloseCancellation,
774 object: ExactObjectRef,
775 ) -> Result<Self, CircleTransitionError> {
776 if object.slot().logical_key()
777 != format!(
778 "{}.json",
779 circle_epoch_close_outcome_semantic_prefix(
780 cancellation.circle_id,
781 cancellation.close_id
782 )
783 )
784 {
785 return Err(CircleTransitionError::InvalidCurrentState);
786 }
787 Ok(Self {
788 close_id: cancellation.close_id,
789 cancellation_hash: cancellation.cancellation_hash(),
790 object,
791 })
792 }
793}
794
795#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
799#[serde(rename_all = "snake_case", deny_unknown_fields)]
800pub enum CircleEpochCloseSlotValue {
801 Outcome(CircleEpochCloseOutcome),
802 Cancellation(CircleEpochCloseCancellation),
803}
804
805impl CircleEpochCloseSlotValue {
806 pub fn to_bytes(&self) -> Vec<u8> {
807 serde_json::to_vec(self).expect("Circle epoch-close slot value serialization cannot fail")
808 }
809
810 pub fn parse(bytes: &[u8]) -> Result<Self, CircleTransitionError> {
811 let value: Self = serde_json::from_slice(bytes)
812 .map_err(|_| CircleTransitionError::InvalidCurrentState)?;
813 if value.to_bytes() != bytes {
814 return Err(CircleTransitionError::InvalidCurrentState);
815 }
816 Ok(value)
817 }
818}