Skip to main content

coven_protocol/circle_control/
epoch_transition.rs

1use super::drafts::*;
2use super::*;
3
4impl CircleTransitionDraft {
5    #[allow(clippy::too_many_arguments)]
6    pub fn close_epoch(
7        candidate_family: crate::store_commit::CandidateFamilyId,
8        device_id: &str,
9        store_membership: StoreMembershipStateRef,
10        membership_authority: MembershipGrantCreationAuthority,
11        store_members: Vec<(String, MemberRole)>,
12        current_control: &PreparedCircleControl,
13        current_roster: &CircleMaterializedRoster,
14        current_metadata: &CircleMetadata,
15        keyring: &str,
16        close_id: CircleEpochCloseId,
17        close_intent: CircleEpochCloseIntent,
18        intent: CircleEpochCloseIntentRef,
19        frozen_device_state: StoreDeviceStateRef,
20        participants: Vec<CircleEpochCloseParticipant>,
21        provisional_frontier: CommitFrontier,
22        outcome_slot: ObjectSlot,
23        ids: &dyn coven_foundation::id_provider::IdProvider,
24        signer: &dyn coven_keys::keys::IdentityKeyAuthority,
25    ) -> Result<Self, CircleTransitionError> {
26        let context = circle_successor_context(
27            store_members,
28            current_control,
29            current_roster,
30            current_metadata,
31            keyring,
32            signer,
33        )?;
34        let CircleSuccessorContext {
35            store_members,
36            author_pubkey,
37            epoch: active_epoch,
38            grant_id,
39            author_authority,
40            key_fingerprint,
41        } = context;
42        let store_root_hash = current_control.value.store_root_hash;
43        let circle_id = current_control.value.circle_id;
44        let epoch_id = current_control.value.epoch_id();
45        if close_intent.close_id != close_id
46            || close_intent.intent_hash() != intent.intent_hash
47            || close_intent.circle_id != circle_id
48            || close_intent.epoch_id != epoch_id
49        {
50            return Err(CircleTransitionError::InvalidCurrentState);
51        }
52        let roster_state = active_epoch.roster.clone();
53        let mut control_value = CircleControlBody {
54            store_root_hash,
55            circle_id,
56            value: CircleControlValue {
57                order: MergeCircleControlOrder {
58                    device_id: device_id.to_string(),
59                    stream_id: active_epoch
60                        .covered_control_heads
61                        .iter()
62                        .find(|head| {
63                            head.coord.author_pubkey == author_pubkey
64                                && head.coord.device_id == device_id
65                                && head.coord.author_owner_grant == grant_id
66                        })
67                        .map_or_else(
68                            || {
69                                AuthorStreamId::from_digest(generated_id_digest(
70                                    ids,
71                                    b"coven.circle-transition-draft-stream.v1\0",
72                                ))
73                            },
74                            |head| head.coord.stream_id,
75                        ),
76                    author_owner_grant: grant_id,
77                    seq: current_control
78                        .value
79                        .ordinal()
80                        .checked_add(1)
81                        .ok_or(CircleTransitionError::SequenceOverflow)?,
82                    previous_control_hash: Some(current_control.coord.control_hash()),
83                    dependencies: vec![current_control.coord.clone()],
84                },
85                state: CircleControlState::EpochClose(CircleEpochClose {
86                    close_id,
87                    frozen_epoch: MergeActiveCircleEpoch {
88                        common: active_epoch.common.clone(),
89                        metadata: active_epoch.metadata.clone(),
90                        roster: roster_state.clone(),
91                        store_membership,
92                        covered_control_heads: active_epoch.covered_control_heads.clone(),
93                    },
94                    intent,
95                    frozen_device_state,
96                    participants,
97                    provisional_frontier,
98                    outcome_slot,
99                }),
100                author_authority,
101                membership_authority,
102            },
103            author_pubkey,
104        };
105        let access = CircleAccessDraft::prepare(
106            store_root_hash,
107            candidate_family,
108            circle_id,
109            epoch_id,
110            keyring,
111            key_fingerprint,
112            &roster_state,
113            &current_roster.members(),
114            &control_value.access_epoch().store_membership,
115            &store_members,
116            &std::collections::BTreeMap::new(),
117            ids,
118            signer,
119        )?;
120        control_value
121            .value
122            .state
123            .access_epoch_mut()
124            .common
125            .access_root = access.access_root();
126        let control_value = Signed::sign(control_value, signer);
127        let control = PreparedCircleControl {
128            coord: control_value.coord(),
129            bytes: serde_json::to_vec(&control_value)
130                .expect("circle control serialization cannot fail"),
131            value: control_value,
132        };
133        let access = access.finish(&control)?;
134        Ok(Self {
135            circle_id,
136            epoch_id,
137            keyring: keyring.to_string(),
138            roster: current_roster.clone(),
139            policy: CircleTransitionDraftPolicy {
140                roster: CircleRosterDraftPolicy::Inherited,
141                metadata_successor: false,
142            },
143            metadata: current_metadata.clone(),
144            close_intent: Some(close_intent),
145            close_finalization: None,
146            close_cancellation: None,
147            access,
148            control,
149        })
150    }
151
152    #[allow(clippy::too_many_arguments)]
153    pub fn finalize_epoch_close(
154        candidate_family: crate::store_commit::CandidateFamilyId,
155        device_id: &str,
156        author_registration: &StoreDeviceRegistrationRef,
157        metadata_stamp: &str,
158        store_membership: StoreMembershipStateRef,
159        membership_authority: MembershipGrantCreationAuthority,
160        mut store_members: Vec<(String, MemberRole)>,
161        close_control: &PreparedCircleControl,
162        current_roster: &CircleMaterializedRoster,
163        current_roster_chain: CircleRosterChain,
164        current_metadata: &CircleMetadata,
165        keyring: &str,
166        intent: CircleEpochCloseIntent,
167        responses: Vec<CircleEpochCloseSettlement>,
168        ids: &dyn coven_foundation::id_provider::IdProvider,
169        signer: &dyn coven_keys::keys::IdentityKeyAuthority,
170    ) -> Result<Self, CircleTransitionError> {
171        let CircleControlState::EpochClose(close) = close_control.value.state() else {
172            return Err(CircleTransitionError::InvalidCurrentState);
173        };
174        if !close_control.verify()
175            || current_roster_chain.try_resolved()? != *current_roster
176            || current_roster.state_hash() != close.frozen_epoch.roster.state_hash
177            || current_metadata.coord() != close.frozen_epoch.metadata.selected
178            || current_metadata.epoch_id != close.frozen_epoch.common.epoch_id
179            || current_metadata.key_fingerprint != close.frozen_epoch.common.key_fingerprint
180            || intent.intent_hash() != close.intent.intent_hash
181            || intent.close_id != close.close_id
182        {
183            return Err(CircleTransitionError::InvalidCurrentState);
184        }
185        let author_pubkey = keys::public_key_hex(signer);
186        store_members.sort_by(|left, right| left.0.cmp(&right.0));
187        store_members.dedup_by(|left, right| left.0 == right.0);
188        if !store_members
189            .iter()
190            .any(|(pubkey, role)| pubkey == &author_pubkey && role.can_write())
191        {
192            return Err(CircleTransitionError::AuthorNotStoreWriter);
193        }
194        let (grant_id, owner_record) = current_roster
195            .active_grants()
196            .find(|(_, record)| {
197                record.member_pubkey == author_pubkey
198                    && record.role == crate::circle::CircleRole::Owner
199            })
200            .ok_or(CircleTransitionError::AuthorNotCircleOwner)?;
201        let author_authority = match &owner_record.creation_authority {
202            CircleGrantCreationAuthority::Entry(created_at) => {
203                MergeCircleOwnerAuthorityRef::Roster {
204                    roster: close.frozen_epoch.roster.clone(),
205                    grant_id: grant_id.clone(),
206                    created_at: created_at.clone(),
207                }
208            }
209            CircleGrantCreationAuthority::ConflictResolution(resolution) => {
210                MergeCircleOwnerAuthorityRef::ConflictResolution {
211                    conflict_hash: resolution.conflict_hash,
212                    resolution_hash: resolution.resolution_hash,
213                }
214            }
215        };
216        let old_encryption = EncryptionService::from(
217            MasterKeyring::from_serialized(keyring)
218                .map_err(|_| CircleTransitionError::InvalidCurrentState)?,
219        );
220        if old_encryption.seal_key_fingerprint() != close.frozen_epoch.common.key_fingerprint {
221            return Err(CircleTransitionError::InvalidCurrentState);
222        }
223        let new_generation = old_encryption
224            .current_generation()
225            .checked_add(1)
226            .ok_or(CircleTransitionError::SequenceOverflow)?;
227        let encryption = old_encryption
228            .with_appended_generation(
229                new_generation,
230                coven_keys::encryption::generate_random_key(),
231            )
232            .map_err(|_| CircleTransitionError::InvalidCurrentState)?;
233        let keyring = encryption
234            .to_keyring_string()
235            .map_err(|_| CircleTransitionError::InvalidCurrentState)?;
236        let key_fingerprint = encryption.seal_key_fingerprint();
237        let epoch_id = CircleEpochId::generate(ids);
238        let roster = current_roster_chain.resolved_with_successor(intent.removal.clone())?;
239        if roster.state_hash() != intent.remaining_roster_state_hash {
240            return Err(CircleTransitionError::InvalidCurrentState);
241        }
242        let roster_members = roster.members();
243        let owners = roster_members
244            .iter()
245            .filter_map(|(pubkey, role)| {
246                (*role == crate::circle::CircleRole::Owner).then_some(pubkey.clone())
247            })
248            .collect::<Vec<_>>();
249        if owners.is_empty() {
250            return Err(CircleTransitionError::InvalidCurrentState);
251        }
252        let roster_state = MergeCircleRosterStateRef {
253            heads: close.frozen_epoch.roster.heads.clone(),
254            resolutions: close.frozen_epoch.roster.resolutions.clone(),
255            state_hash: roster.state_hash(),
256        };
257        let metadata_stream = crate::store_commit::StreamActivation::grant_authorized_stream_id(
258            close_control.value.store_root_hash,
259            author_registration,
260            grant_id,
261            crate::store_commit::StreamAnchorDomain::CircleMetadata {
262                circle_id: close_control.value.circle_id,
263            },
264        );
265        let prior_metadata = close
266            .frozen_epoch
267            .metadata
268            .heads
269            .iter()
270            .find(|head| head.coord.stream_id == metadata_stream);
271        let mut metadata = current_metadata.body().clone();
272        metadata.epoch_id = epoch_id;
273        metadata.metadata_stamp = metadata_stamp.to_string();
274        metadata.author_pubkey = author_pubkey.clone();
275        metadata.device_id = device_id.to_string();
276        metadata.stream_id = metadata_stream;
277        metadata.author_owner_grant = grant_id.clone();
278        metadata.seq = prior_metadata.map_or(Ok(1), |head| {
279            head.coord
280                .seq
281                .checked_add(1)
282                .ok_or(CircleTransitionError::SequenceOverflow)
283        })?;
284        metadata.previous_hash = prior_metadata.map(|head| head.coord.metadata_hash);
285        metadata.dependencies = close
286            .frozen_epoch
287            .metadata
288            .heads
289            .iter()
290            .map(|head| head.coord.clone())
291            .collect();
292        metadata.author_roster = roster_state.clone();
293        metadata.key_fingerprint = key_fingerprint;
294        let metadata = Signed::sign(metadata, signer);
295        let metadata_state = MergeCircleMetadataStateRef {
296            heads: close.frozen_epoch.metadata.heads.clone(),
297            selected: metadata.coord(),
298            state_hash: metadata.metadata_hash(),
299        };
300        let mut control_value = CircleControlBody {
301            store_root_hash: close_control.value.store_root_hash,
302            circle_id: close_control.value.circle_id,
303            value: CircleControlValue {
304                order: MergeCircleControlOrder {
305                    device_id: device_id.to_string(),
306                    stream_id: close_control.value.value.order.stream_id,
307                    author_owner_grant: grant_id.clone(),
308                    seq: close_control
309                        .value
310                        .ordinal()
311                        .checked_add(1)
312                        .ok_or(CircleTransitionError::SequenceOverflow)?,
313                    previous_control_hash: Some(close_control.coord.control_hash()),
314                    dependencies: vec![close_control.coord.clone()],
315                },
316                state: CircleControlState::ActiveEpoch(MergeActiveCircleEpoch {
317                    common: ActiveCircleEpochCore {
318                        epoch_id,
319                        key_fingerprint,
320                        owners,
321                        access_root: close.frozen_epoch.common.access_root,
322                        origin: close.frozen_epoch.common.origin.clone(),
323                    },
324                    metadata: metadata_state,
325                    roster: roster_state.clone(),
326                    store_membership,
327                    covered_control_heads: close.frozen_epoch.covered_control_heads.clone(),
328                }),
329                author_authority,
330                membership_authority,
331            },
332            author_pubkey,
333        };
334        let access = CircleAccessDraft::prepare(
335            control_value.store_root_hash,
336            candidate_family,
337            control_value.circle_id,
338            epoch_id,
339            &keyring,
340            key_fingerprint,
341            &roster_state,
342            &roster_members,
343            &control_value.access_epoch().store_membership,
344            &store_members,
345            &std::collections::BTreeMap::new(),
346            ids,
347            signer,
348        )?;
349        control_value
350            .value
351            .state
352            .active_epoch_mut()
353            .expect("Circle finalization constructs an active epoch")
354            .common
355            .access_root = access.access_root();
356        let control_value = Signed::sign(control_value, signer);
357        let control = PreparedCircleControl {
358            coord: control_value.coord(),
359            bytes: serde_json::to_vec(&control_value)
360                .expect("Circle control serialization cannot fail"),
361            value: control_value,
362        };
363        let access = access.finish(&control)?;
364        Ok(Self {
365            circle_id: control.value.circle_id,
366            epoch_id,
367            keyring,
368            roster,
369            policy: CircleTransitionDraftPolicy {
370                roster: CircleRosterDraftPolicy::Successor {
371                    predecessor: current_roster_chain,
372                    entry: intent.removal.clone(),
373                },
374                metadata_successor: true,
375            },
376            metadata,
377            close_intent: None,
378            close_finalization: Some(CircleEpochCloseFinalizationDraft {
379                close_control: close_control.clone(),
380                intent,
381                responses,
382                outcome_slot: close.outcome_slot.clone(),
383            }),
384            close_cancellation: None,
385            access,
386            control,
387        })
388    }
389
390    /// Reopen a frozen epoch by cancelling its close. The successor restores the
391    /// frozen epoch's protocol identity — same epoch, key generation, roster and
392    /// metadata frontiers, and origin — re-issuing only the control-bound access
393    /// material to the reopening control.
394    #[allow(clippy::too_many_arguments)]
395    pub fn reopen_epoch(
396        candidate_family: crate::store_commit::CandidateFamilyId,
397        device_id: &str,
398        store_membership: StoreMembershipStateRef,
399        membership_authority: MembershipGrantCreationAuthority,
400        mut store_members: Vec<(String, MemberRole)>,
401        close_control: &PreparedCircleControl,
402        current_roster: &CircleMaterializedRoster,
403        current_metadata: &CircleMetadata,
404        keyring: &str,
405        ids: &dyn coven_foundation::id_provider::IdProvider,
406        signer: &dyn coven_keys::keys::IdentityKeyAuthority,
407    ) -> Result<Self, CircleTransitionError> {
408        let CircleControlState::EpochClose(close) = close_control.value.state() else {
409            return Err(CircleTransitionError::InvalidCurrentState);
410        };
411        let frozen = &close.frozen_epoch;
412        if !close_control.verify()
413            || current_roster.state_hash() != frozen.roster.state_hash
414            || current_metadata.coord() != frozen.metadata.selected
415            || current_metadata.epoch_id != frozen.common.epoch_id
416            || current_metadata.key_fingerprint != frozen.common.key_fingerprint
417        {
418            return Err(CircleTransitionError::InvalidCurrentState);
419        }
420        let author_pubkey = keys::public_key_hex(signer);
421        store_members.sort_by(|left, right| left.0.cmp(&right.0));
422        store_members.dedup_by(|left, right| left.0 == right.0);
423        if !store_members
424            .iter()
425            .any(|(pubkey, role)| pubkey == &author_pubkey && role.can_write())
426        {
427            return Err(CircleTransitionError::AuthorNotStoreWriter);
428        }
429        let (grant_id, owner_record) = current_roster
430            .active_grants()
431            .find(|(_, record)| {
432                record.member_pubkey == author_pubkey
433                    && record.role == crate::circle::CircleRole::Owner
434            })
435            .ok_or(CircleTransitionError::AuthorNotCircleOwner)?;
436        let author_authority = match &owner_record.creation_authority {
437            CircleGrantCreationAuthority::Entry(created_at) => {
438                MergeCircleOwnerAuthorityRef::Roster {
439                    roster: frozen.roster.clone(),
440                    grant_id: grant_id.clone(),
441                    created_at: created_at.clone(),
442                }
443            }
444            CircleGrantCreationAuthority::ConflictResolution(resolution) => {
445                MergeCircleOwnerAuthorityRef::ConflictResolution {
446                    conflict_hash: resolution.conflict_hash,
447                    resolution_hash: resolution.resolution_hash,
448                }
449            }
450        };
451        let encryption = EncryptionService::from(
452            MasterKeyring::from_serialized(keyring)
453                .map_err(|_| CircleTransitionError::InvalidCurrentState)?,
454        );
455        let key_fingerprint = encryption.seal_key_fingerprint();
456        if key_fingerprint != frozen.common.key_fingerprint {
457            return Err(CircleTransitionError::InvalidCurrentState);
458        }
459        let epoch_id = frozen.common.epoch_id;
460        let roster_state = frozen.roster.clone();
461        let mut control_value = CircleControlBody {
462            store_root_hash: close_control.value.store_root_hash,
463            circle_id: close_control.value.circle_id,
464            value: CircleControlValue {
465                order: MergeCircleControlOrder {
466                    device_id: device_id.to_string(),
467                    stream_id: close_control.value.value.order.stream_id,
468                    author_owner_grant: grant_id.clone(),
469                    seq: close_control
470                        .value
471                        .ordinal()
472                        .checked_add(1)
473                        .ok_or(CircleTransitionError::SequenceOverflow)?,
474                    previous_control_hash: Some(close_control.coord.control_hash()),
475                    dependencies: vec![close_control.coord.clone()],
476                },
477                state: CircleControlState::ActiveEpoch(MergeActiveCircleEpoch {
478                    common: ActiveCircleEpochCore {
479                        epoch_id,
480                        key_fingerprint,
481                        owners: frozen.common.owners.clone(),
482                        access_root: frozen.common.access_root,
483                        origin: frozen.common.origin.clone(),
484                    },
485                    metadata: frozen.metadata.clone(),
486                    roster: roster_state.clone(),
487                    store_membership,
488                    covered_control_heads: frozen.covered_control_heads.clone(),
489                }),
490                author_authority,
491                membership_authority,
492            },
493            author_pubkey,
494        };
495        let access = CircleAccessDraft::prepare(
496            control_value.store_root_hash,
497            candidate_family,
498            control_value.circle_id,
499            epoch_id,
500            keyring,
501            key_fingerprint,
502            &roster_state,
503            &current_roster.members(),
504            &control_value.access_epoch().store_membership,
505            &store_members,
506            &std::collections::BTreeMap::new(),
507            ids,
508            signer,
509        )?;
510        control_value
511            .value
512            .state
513            .active_epoch_mut()
514            .expect("Circle reopen constructs an active epoch")
515            .common
516            .access_root = access.access_root();
517        let control_value = Signed::sign(control_value, signer);
518        let control = PreparedCircleControl {
519            coord: control_value.coord(),
520            bytes: serde_json::to_vec(&control_value)
521                .expect("Circle control serialization cannot fail"),
522            value: control_value,
523        };
524        let access = access.finish(&control)?;
525        Ok(Self {
526            circle_id: control.value.circle_id,
527            epoch_id,
528            keyring: keyring.to_string(),
529            roster: current_roster.clone(),
530            policy: CircleTransitionDraftPolicy {
531                roster: CircleRosterDraftPolicy::Inherited,
532                metadata_successor: false,
533            },
534            metadata: current_metadata.clone(),
535            close_intent: None,
536            close_finalization: None,
537            close_cancellation: Some(CircleEpochCloseCancellationDraft {
538                close_control: close_control.clone(),
539                outcome_slot: close.outcome_slot.clone(),
540            }),
541            access,
542            control,
543        })
544    }
545}