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 ¤t_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 #[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 ¤t_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}