1use hmac::{Hmac, Mac};
4use serde::{Deserialize, Serialize};
5use sha2::Sha256;
6
7use super::causal_grants::AuthorStreamId;
8use super::circle::CircleEpochCloseId;
9use super::circle::{generated_id_digest, AccessLeafId, CircleEpochId, CircleId};
10use super::circle_roster::{
11 CircleAuthorStreamKey, CircleGrantCreationAuthority, CircleMaterializedRoster,
12 CircleRosterChain, CircleRosterEntry, CircleRosterError, CircleRosterHead, CircleRosterHeadRef,
13 CircleRosterStateRef, MergeCircleRosterStateRef, ResolvedCircleRoster,
14};
15use super::membership::{MemberRole, MembershipGrantCreationAuthority, MembershipGrantId};
16use super::membership::{MembershipHeadRef, StoreMembershipConflictResolutionRef};
17use super::storage::ExactObjectRef;
18use super::store_commit::{
19 CommitFrontier, ObjectHash, OwnerRecoveryCursor, SnapshotImageRef, StoreBatchCommitRef,
20 StoreDeviceRegistration, StoreDeviceRegistrationRef, StoreDeviceStateRef, SuccessorLink,
21 STORE_PROTOCOL_VERSION,
22};
23use crate::encryption::{EncryptionService, KeyFingerprint, MasterKeyring};
24use crate::keys::{self, UserKeypair};
25use crate::storage::cloud::ObjectSlot;
26
27const RECIPIENT_SLOT_DOMAIN: &[u8] = b"coven.circle-recipient-slot.v1\0";
28const METADATA_DOMAIN: &str = "coven.circle-metadata.v1";
29const METADATA_HEAD_DOMAIN: &str = "coven.circle-metadata-head.v1";
30const ACCESS_DOMAIN: &str = "coven.circle-access-leaf.v1";
31const CONTROL_DOMAIN: &str = "coven.circle-control.v1";
32const ENVELOPE_DOMAIN: &str = "coven.circle-access-envelope.v1";
33const OWNER_GRANT_ID_GENERATION_DOMAIN: &[u8] = b"coven.circle-owner-grant-id-generation.v1\0";
34
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct CircleControlCoord {
39 pub device_id: String,
40 pub stream_id: AuthorStreamId,
41 pub author_pubkey: String,
42 pub author_owner_grant: MembershipGrantId,
43 pub seq: u64,
44 pub control_hash: ObjectHash,
45}
46
47impl CircleControlCoord {
48 pub fn control_hash(&self) -> ObjectHash {
49 self.control_hash
50 }
51
52 pub fn validate(&self) -> Result<(), CircleControlCoordError> {
53 if self.device_id.is_empty() || self.author_pubkey.is_empty() || self.seq == 0 {
54 Err(CircleControlCoordError)
55 } else {
56 Ok(())
57 }
58 }
59
60 pub fn stream_key(&self) -> CircleAuthorStreamKey {
61 CircleAuthorStreamKey {
62 author_pubkey: self.author_pubkey.clone(),
63 device_id: self.device_id.clone(),
64 stream_id: self.stream_id,
65 author_owner_grant: self.author_owner_grant.clone(),
66 }
67 }
68
69 #[cfg(any(test, feature = "test-utils"))]
72 pub fn placeholder(seed: u8) -> Self {
73 let hash = ObjectHash::digest(&[seed]);
74 Self {
75 device_id: format!("device-{seed}"),
76 stream_id: AuthorStreamId::from_digest(hash),
77 author_pubkey: format!("pubkey-{seed}"),
78 author_owner_grant: MembershipGrantId(hash),
79 seq: 1,
80 control_hash: hash,
81 }
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
86#[error("circle control coordinate has an empty device/author or zero sequence/generation")]
87pub struct CircleControlCoordError;
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(deny_unknown_fields)]
92pub struct StoreMembershipStateRef {
93 pub heads: Vec<MembershipHeadRef>,
94 pub resolutions: Vec<StoreMembershipConflictResolutionRef>,
95 pub recovery: Vec<OwnerRecoveryCursor>,
96 pub state_hash: ObjectHash,
97}
98
99impl StoreMembershipStateRef {
100 pub fn from_parts(
101 mut heads: Vec<MembershipHeadRef>,
102 mut resolutions: Vec<StoreMembershipConflictResolutionRef>,
103 recovery: Vec<OwnerRecoveryCursor>,
104 membership_state_hash: ObjectHash,
105 ) -> Result<Self, super::store_commit::StoreProtocolError> {
106 heads.sort();
107 resolutions.sort();
108 let recovery = super::store_commit::canonical_recovery_cursors(recovery)?;
109 Ok(Self {
110 heads,
111 resolutions,
112 state_hash: membership_state_ref_hash(membership_state_hash, &recovery),
113 recovery,
114 })
115 }
116
117 pub fn state_hash(&self) -> ObjectHash {
118 self.state_hash
119 }
120
121 pub fn recovery(&self) -> &[OwnerRecoveryCursor] {
122 &self.recovery
123 }
124
125 pub(crate) fn validate_shape(&self) -> Result<(), super::store_commit::StoreProtocolError> {
126 super::store_commit::validate_recovery_cursors(self.recovery())?;
127 if self.heads.windows(2).any(|pair| pair[0] >= pair[1])
128 || self.resolutions.windows(2).any(|pair| pair[0] >= pair[1])
129 {
130 return Err(super::store_commit::StoreProtocolError::Malformed(
131 "Store membership state reference is not canonical".to_string(),
132 ));
133 }
134 Ok(())
135 }
136}
137
138fn membership_state_ref_hash(
139 membership_state_hash: ObjectHash,
140 recovery: &[OwnerRecoveryCursor],
141) -> ObjectHash {
142 ObjectHash::digest(
143 &serde_json::to_vec(&(
144 "coven.store-membership-state-ref.v1",
145 membership_state_hash,
146 recovery,
147 ))
148 .expect("Store membership state hash serialization cannot fail"),
149 )
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
153#[serde(deny_unknown_fields)]
154pub struct CircleMetadataCoord {
155 pub author_pubkey: String,
156 pub device_id: String,
157 pub stream_id: AuthorStreamId,
158 pub author_owner_grant: MembershipGrantId,
159 pub seq: u64,
160 pub metadata_hash: ObjectHash,
161}
162
163impl CircleMetadataCoord {
164 pub fn stream_key(&self) -> CircleAuthorStreamKey {
165 CircleAuthorStreamKey {
166 author_pubkey: self.author_pubkey.clone(),
167 device_id: self.device_id.clone(),
168 stream_id: self.stream_id,
169 author_owner_grant: self.author_owner_grant.clone(),
170 }
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(deny_unknown_fields)]
176pub struct CircleMetadata {
177 pub version: u32,
178 pub store_root_hash: ObjectHash,
179 pub circle_id: CircleId,
180 pub epoch_id: CircleEpochId,
181 pub name: String,
182 pub seq: u64,
183 pub previous_hash: Option<ObjectHash>,
184 pub dependencies: Vec<CircleMetadataCoord>,
185 pub metadata_stamp: String,
186 pub author_pubkey: String,
187 pub device_id: String,
188 pub stream_id: AuthorStreamId,
189 pub author_owner_grant: MembershipGrantId,
190 pub author_roster: CircleRosterStateRef,
191 pub key_fingerprint: KeyFingerprint,
192 pub signature: String,
193}
194
195impl CircleMetadata {
196 fn founder(
197 store_root_hash: ObjectHash,
198 circle_id: CircleId,
199 epoch_id: CircleEpochId,
200 name: &str,
201 metadata_stamp: &str,
202 device_id: &str,
203 stream_id: AuthorStreamId,
204 owner_grant: MembershipGrantId,
205 author_roster: CircleRosterStateRef,
206 key_fingerprint: KeyFingerprint,
207 signer: &UserKeypair,
208 ) -> Result<Self, CircleTransitionError> {
209 if name.trim().is_empty() {
210 return Err(CircleTransitionError::EmptyName);
211 }
212 let author_pubkey = keys::public_key_hex(signer);
213 let mut metadata = Self {
214 version: STORE_PROTOCOL_VERSION,
215 store_root_hash,
216 circle_id,
217 epoch_id,
218 name: name.to_string(),
219 seq: 1,
220 previous_hash: None,
221 dependencies: Vec::new(),
222 metadata_stamp: metadata_stamp.to_string(),
223 author_pubkey,
224 device_id: device_id.to_string(),
225 stream_id,
226 author_owner_grant: owner_grant,
227 author_roster,
228 key_fingerprint,
229 signature: String::new(),
230 };
231 metadata.signature = keys::sign_hex(signer, &metadata.canonical_bytes()).1;
232 Ok(metadata)
233 }
234
235 pub(crate) fn canonical_bytes(&self) -> Vec<u8> {
236 #[derive(Serialize)]
237 struct Signed<'a> {
238 domain: &'static str,
239 version: u32,
240 store_root_hash: ObjectHash,
241 circle_id: CircleId,
242 epoch_id: CircleEpochId,
243 name: &'a str,
244 seq: u64,
245 previous_hash: Option<ObjectHash>,
246 dependencies: &'a [CircleMetadataCoord],
247 metadata_stamp: &'a str,
248 author_pubkey: &'a str,
249 device_id: &'a str,
250 stream_id: AuthorStreamId,
251 author_owner_grant: &'a MembershipGrantId,
252 author_roster: &'a CircleRosterStateRef,
253 key_fingerprint: KeyFingerprint,
254 }
255 serde_json::to_vec(&Signed {
256 domain: METADATA_DOMAIN,
257 version: self.version,
258 store_root_hash: self.store_root_hash,
259 circle_id: self.circle_id,
260 epoch_id: self.epoch_id,
261 name: &self.name,
262 seq: self.seq,
263 previous_hash: self.previous_hash,
264 dependencies: &self.dependencies,
265 metadata_stamp: &self.metadata_stamp,
266 author_pubkey: &self.author_pubkey,
267 device_id: &self.device_id,
268 stream_id: self.stream_id,
269 author_owner_grant: &self.author_owner_grant,
270 author_roster: &self.author_roster,
271 key_fingerprint: self.key_fingerprint,
272 })
273 .expect("circle metadata serialization cannot fail")
274 }
275
276 pub fn metadata_hash(&self) -> ObjectHash {
277 ObjectHash::digest(
278 &serde_json::to_vec(self).expect("circle metadata serialization cannot fail"),
279 )
280 }
281
282 pub fn coord(&self) -> CircleMetadataCoord {
283 CircleMetadataCoord {
284 author_pubkey: self.author_pubkey.clone(),
285 device_id: self.device_id.clone(),
286 stream_id: self.stream_id,
287 author_owner_grant: self.author_owner_grant.clone(),
288 seq: self.seq,
289 metadata_hash: self.metadata_hash(),
290 }
291 }
292
293 pub fn verify(&self) -> bool {
294 let position_is_valid = (self.seq == 1
295 && self.previous_hash.is_none()
296 && self
297 .dependencies
298 .iter()
299 .all(|dependency| dependency.stream_key() != self.coord().stream_key()))
300 || (self.seq > 1 && self.previous_hash.is_some());
301 self.version == STORE_PROTOCOL_VERSION
302 && !self.name.trim().is_empty()
303 && position_is_valid
304 && self
305 .dependencies
306 .windows(2)
307 .all(|pair| pair[0].stream_key() < pair[1].stream_key())
308 && keys::verify_signature_hex(
309 &self.author_pubkey,
310 &self.signature,
311 &self.canonical_bytes(),
312 )
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
317#[serde(deny_unknown_fields)]
318pub struct CircleMetadataHead {
319 pub version: u32,
320 pub store_root_hash: ObjectHash,
321 pub circle_id: CircleId,
322 pub author_pubkey: String,
323 pub device_id: String,
324 pub stream_id: AuthorStreamId,
325 pub author_owner_grant: MembershipGrantId,
326 pub seq: u64,
327 pub tip_hash: ObjectHash,
328 pub tip: ExactObjectRef,
329 pub successor: SuccessorLink,
330 pub signature: String,
331}
332
333impl CircleMetadataHead {
334 pub(crate) fn signed(
335 metadata: &CircleMetadata,
336 tip: ExactObjectRef,
337 successor: SuccessorLink,
338 signer: &UserKeypair,
339 ) -> Self {
340 let mut head = Self {
341 version: STORE_PROTOCOL_VERSION,
342 store_root_hash: metadata.store_root_hash,
343 circle_id: metadata.circle_id,
344 author_pubkey: metadata.author_pubkey.clone(),
345 device_id: metadata.device_id.clone(),
346 stream_id: metadata.stream_id,
347 author_owner_grant: metadata.author_owner_grant.clone(),
348 seq: metadata.seq,
349 tip_hash: metadata.metadata_hash(),
350 tip,
351 successor,
352 signature: String::new(),
353 };
354 head.signature = keys::sign_hex(signer, &head.canonical_bytes()).1;
355 head
356 }
357
358 pub(crate) fn canonical_bytes(&self) -> Vec<u8> {
359 #[derive(Serialize)]
360 struct Signed<'a> {
361 domain: &'static str,
362 version: u32,
363 store_root_hash: ObjectHash,
364 circle_id: CircleId,
365 author_pubkey: &'a str,
366 device_id: &'a str,
367 stream_id: AuthorStreamId,
368 author_owner_grant: &'a MembershipGrantId,
369 seq: u64,
370 tip_hash: ObjectHash,
371 tip: &'a ExactObjectRef,
372 successor: &'a SuccessorLink,
373 }
374 serde_json::to_vec(&Signed {
375 domain: METADATA_HEAD_DOMAIN,
376 version: self.version,
377 store_root_hash: self.store_root_hash,
378 circle_id: self.circle_id,
379 author_pubkey: &self.author_pubkey,
380 device_id: &self.device_id,
381 stream_id: self.stream_id,
382 author_owner_grant: &self.author_owner_grant,
383 seq: self.seq,
384 tip_hash: self.tip_hash,
385 tip: &self.tip,
386 successor: &self.successor,
387 })
388 .expect("circle metadata head serialization cannot fail")
389 }
390
391 pub fn head_hash(&self) -> ObjectHash {
392 ObjectHash::digest(
393 &serde_json::to_vec(self).expect("circle metadata head serialization cannot fail"),
394 )
395 }
396
397 pub fn coord(&self) -> CircleMetadataCoord {
398 CircleMetadataCoord {
399 author_pubkey: self.author_pubkey.clone(),
400 device_id: self.device_id.clone(),
401 stream_id: self.stream_id,
402 author_owner_grant: self.author_owner_grant.clone(),
403 seq: self.seq,
404 metadata_hash: self.tip_hash,
405 }
406 }
407
408 pub fn verify_for_registration(&self, registration: &StoreDeviceRegistration) -> bool {
409 self.version == STORE_PROTOCOL_VERSION
410 && self.seq > 0
411 && !self.device_id.is_empty()
412 && self.device_id == registration.device_id.to_string()
413 && keys::verify_signature_hex(
414 ®istration.device_signing_pubkey,
415 &self.signature,
416 &self.canonical_bytes(),
417 )
418 }
419}
420
421#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
422#[serde(deny_unknown_fields)]
423pub struct CircleMetadataHeadRef {
424 pub coord: CircleMetadataCoord,
425 pub head_hash: ObjectHash,
426 pub object: ExactObjectRef,
427}
428
429impl CircleMetadataHeadRef {
430 pub(crate) fn from_stored_head(head: &CircleMetadataHead, object: ExactObjectRef) -> Self {
431 Self {
432 coord: head.coord(),
433 head_hash: head.head_hash(),
434 object,
435 }
436 }
437}
438
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
440#[serde(deny_unknown_fields)]
441pub struct MergeCircleMetadataStateRef {
442 pub heads: Vec<CircleMetadataHeadRef>,
443 pub selected: CircleMetadataCoord,
444 pub state_hash: ObjectHash,
445}
446
447pub type CircleMetadataStateRef = MergeCircleMetadataStateRef;
448
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
451#[serde(deny_unknown_fields)]
452pub struct CircleBootstrapRef {
453 pub coverage: CommitFrontier,
454 pub schema_version: u32,
455 pub sync_routing_hash: ObjectHash,
456 pub image: SnapshotImageRef,
457 pub blobs: Vec<crate::blob::RowBlobRef>,
458}
459
460impl CircleBootstrapRef {
461 pub(crate) fn verify_for_access(&self, access: &CircleAccessLeaf) -> bool {
462 if super::store_commit::validate_commit_frontier(&self.coverage).is_err() {
463 return false;
464 }
465 let blobs_are_canonical = self.blobs.windows(2).all(|pair| {
466 serde_json::to_vec(&pair[0]).expect("row blob reference serialization cannot fail")
467 < serde_json::to_vec(&pair[1])
468 .expect("row blob reference serialization cannot fail")
469 });
470 if !blobs_are_canonical
471 || self.blobs.iter().any(|blob| {
472 !matches!(
473 blob.authority(),
474 crate::blob::RowBlobAuthority::Remote(
475 super::audience_package::PackageAudience::Circle {
476 circle_id,
477 ..
478 }
479 ) if *circle_id == access.circle_id
480 ) || blob.stored().is_none_or(|stored| {
481 stored.locator().audience()
482 != crate::blob::locator::RemoteAudience::Circle(access.circle_id)
483 })
484 })
485 {
486 return false;
487 }
488 let semantic_prefix = super::store_commit::circle_bootstrap_image_semantic_prefix(
489 access.circle_id,
490 access.candidate_family,
491 &access.owner_pubkey,
492 access.epoch_id,
493 &access.recipient_slot,
494 self.image.image_hash,
495 );
496 self.image.object.slot().logical_key() == format!("{semantic_prefix}.db")
497 }
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
506#[serde(deny_unknown_fields)]
507pub struct CircleBootstrapCoverageRef {
508 pub circle_id: CircleId,
509 pub control: CircleControlCoord,
510 pub activation_commit: StoreBatchCommitRef,
511 pub bootstrap: CircleBootstrapRef,
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
515#[serde(rename_all = "snake_case", deny_unknown_fields)]
516pub enum CircleAccessDisposition {
517 Active {
518 keyring: String,
519 key_fingerprint: KeyFingerprint,
520 roster: CircleRosterStateRef,
521 bootstrap: Option<CircleBootstrapRef>,
522 },
523 Inactive,
524}
525
526#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
527#[serde(deny_unknown_fields)]
528pub struct CircleAccessLeaf {
529 pub version: u32,
530 pub store_root_hash: ObjectHash,
531 pub candidate_family: super::store_commit::CandidateFamilyId,
532 pub circle_id: CircleId,
533 pub epoch_id: CircleEpochId,
534 pub leaf_id: AccessLeafId,
535 pub owner_pubkey: String,
536 pub recipient_pubkey: String,
537 pub recipient_slot: String,
538 pub disposition: CircleAccessDisposition,
539 pub store_membership: StoreMembershipStateRef,
540 pub signature: String,
541}
542
543impl CircleAccessLeaf {
544 pub(crate) fn canonical_bytes(&self) -> Vec<u8> {
545 #[derive(Serialize)]
546 struct Signed<'a> {
547 domain: &'static str,
548 version: u32,
549 store_root_hash: ObjectHash,
550 candidate_family: super::store_commit::CandidateFamilyId,
551 circle_id: CircleId,
552 epoch_id: CircleEpochId,
553 leaf_id: AccessLeafId,
554 owner_pubkey: &'a str,
555 recipient_pubkey: &'a str,
556 recipient_slot: &'a str,
557 disposition: &'a CircleAccessDisposition,
558 store_membership: &'a StoreMembershipStateRef,
559 }
560 serde_json::to_vec(&Signed {
561 domain: ACCESS_DOMAIN,
562 version: self.version,
563 store_root_hash: self.store_root_hash,
564 candidate_family: self.candidate_family,
565 circle_id: self.circle_id,
566 epoch_id: self.epoch_id,
567 leaf_id: self.leaf_id,
568 owner_pubkey: &self.owner_pubkey,
569 recipient_pubkey: &self.recipient_pubkey,
570 recipient_slot: &self.recipient_slot,
571 disposition: &self.disposition,
572 store_membership: &self.store_membership,
573 })
574 .expect("circle access serialization cannot fail")
575 }
576
577 pub fn verify_signature(&self) -> bool {
578 self.version == STORE_PROTOCOL_VERSION
579 && keys::verify_signature_hex(
580 &self.owner_pubkey,
581 &self.signature,
582 &self.canonical_bytes(),
583 )
584 }
585
586 pub(crate) fn verify_for_control(
587 &self,
588 control: &PreparedCircleControl,
589 candidate_family: super::store_commit::CandidateFamilyId,
590 ) -> bool {
591 self.verify_signature()
592 && self.store_root_hash == control.value.store_root_hash
593 && self.candidate_family == candidate_family
594 && self.circle_id == control.value.circle_id
595 && self.epoch_id == control.value.epoch_id()
596 && self.store_membership == control.value.store_membership_state_ref()
597 && match &self.disposition {
598 CircleAccessDisposition::Active {
599 keyring,
600 key_fingerprint,
601 roster,
602 bootstrap,
603 } => {
604 roster == &control.value.roster_state_ref()
605 && *key_fingerprint == control.value.key_fingerprint()
606 && MasterKeyring::from_serialized(keyring).is_ok_and(|keyring| {
607 EncryptionService::from(keyring).seal_key_fingerprint()
608 == *key_fingerprint
609 })
610 && bootstrap
611 .as_ref()
612 .is_none_or(|bootstrap| bootstrap.verify_for_access(self))
613 }
614 CircleAccessDisposition::Inactive => true,
615 }
616 && self.owner_pubkey == control.value.author_pubkey
617 }
618}
619
620#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
621#[serde(rename_all = "snake_case", deny_unknown_fields)]
622pub enum MerkleStep {
623 Left(ObjectHash),
624 Right(ObjectHash),
625}
626
627fn merkle_parent(left: ObjectHash, right: ObjectHash) -> ObjectHash {
628 let mut bytes = Vec::with_capacity(1 + 64);
629 bytes.push(1);
630 bytes.extend_from_slice(left.as_bytes());
631 bytes.extend_from_slice(right.as_bytes());
632 ObjectHash::digest(&bytes)
633}
634
635pub(crate) fn verify_merkle_proof(
636 mut hash: ObjectHash,
637 proof: &[MerkleStep],
638 root: ObjectHash,
639) -> bool {
640 for step in proof {
641 hash = match step {
642 MerkleStep::Left(left) => merkle_parent(*left, hash),
643 MerkleStep::Right(right) => merkle_parent(hash, *right),
644 };
645 }
646 hash == root
647}
648
649pub(crate) fn merkle_root_and_proofs(hashes: &[ObjectHash]) -> (ObjectHash, Vec<Vec<MerkleStep>>) {
650 assert!(
651 !hashes.is_empty(),
652 "a circle control has at least one access leaf"
653 );
654 let mut indexed = hashes
655 .iter()
656 .copied()
657 .enumerate()
658 .collect::<Vec<(usize, ObjectHash)>>();
659 indexed.sort_by_key(|(index, hash)| (*hash, *index));
660 let mut proofs = vec![Vec::new(); hashes.len()];
661 let mut layer = indexed
662 .into_iter()
663 .map(|(index, hash)| (hash, vec![index]))
664 .collect::<Vec<_>>();
665 while layer.len() > 1 {
666 let mut next = Vec::with_capacity(layer.len().div_ceil(2));
667 for pair in layer.chunks(2) {
668 let (left_hash, left_indices) = &pair[0];
669 if let Some((right_hash, right_indices)) = pair.get(1) {
670 for index in left_indices {
671 proofs[*index].push(MerkleStep::Right(*right_hash));
672 }
673 for index in right_indices {
674 proofs[*index].push(MerkleStep::Left(*left_hash));
675 }
676 let mut indices = left_indices.clone();
677 indices.extend(right_indices);
678 next.push((merkle_parent(*left_hash, *right_hash), indices));
679 } else {
680 for index in left_indices {
681 proofs[*index].push(MerkleStep::Right(*left_hash));
682 }
683 next.push((merkle_parent(*left_hash, *left_hash), left_indices.clone()));
684 }
685 }
686 layer = next;
687 }
688 (layer[0].0, proofs)
689}
690
691#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
692#[serde(deny_unknown_fields)]
693pub struct MergeCircleControlOrder {
694 pub device_id: String,
695 pub stream_id: AuthorStreamId,
696 pub author_owner_grant: MembershipGrantId,
697 pub seq: u64,
698 pub previous_control_hash: Option<ObjectHash>,
699 pub dependencies: Vec<CircleControlCoord>,
700}
701
702#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
703#[serde(deny_unknown_fields)]
704pub struct ActiveCircleEpochCore {
705 pub epoch_id: CircleEpochId,
706 pub key_fingerprint: KeyFingerprint,
707 pub owners: Vec<String>,
708 pub access_root: ObjectHash,
709 pub origin: CircleEpochOrigin,
710}
711
712#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
713#[serde(rename_all = "snake_case", deny_unknown_fields)]
714pub enum CircleEpochOrigin {
715 Founder,
716 Closed {
717 closed_epoch_id: CircleEpochId,
718 close_control: CircleControlCoord,
719 close_id: CircleEpochCloseId,
720 outcome_hash: ObjectHash,
721 cutoff: CommitFrontier,
722 },
723}
724
725#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
726#[serde(deny_unknown_fields)]
727pub struct MergeActiveCircleEpoch {
728 pub common: ActiveCircleEpochCore,
729 pub metadata: MergeCircleMetadataStateRef,
730 pub roster: MergeCircleRosterStateRef,
731 pub store_membership: StoreMembershipStateRef,
732 pub covered_control_heads: Vec<MergeCircleControlHeadRef>,
733}
734
735#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
736#[serde(deny_unknown_fields)]
737pub struct CircleEpochCloseParticipant {
738 pub registration: StoreDeviceRegistrationRef,
739 pub response_slot: ObjectSlot,
740}
741
742#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
743#[serde(deny_unknown_fields)]
744pub struct CircleEpochCloseIntent {
745 pub version: u32,
746 pub store_root_hash: ObjectHash,
747 pub circle_id: CircleId,
748 pub close_id: CircleEpochCloseId,
749 pub epoch_id: CircleEpochId,
750 pub predecessor_roster: MergeCircleRosterStateRef,
751 pub removal: CircleRosterEntry,
752 pub remaining_roster_state_hash: ObjectHash,
753 pub owner_pubkey: String,
754 pub signature: String,
755}
756
757impl CircleEpochCloseIntent {
758 #[allow(clippy::too_many_arguments)]
759 pub(crate) fn signed(
760 store_root_hash: ObjectHash,
761 circle_id: CircleId,
762 close_id: CircleEpochCloseId,
763 epoch_id: CircleEpochId,
764 predecessor_roster: MergeCircleRosterStateRef,
765 removal: CircleRosterEntry,
766 remaining_roster_state_hash: ObjectHash,
767 signer: &UserKeypair,
768 ) -> Result<Self, CircleTransitionError> {
769 let owner_pubkey = keys::public_key_hex(signer);
770 let mut intent = Self {
771 version: STORE_PROTOCOL_VERSION,
772 store_root_hash,
773 circle_id,
774 close_id,
775 epoch_id,
776 predecessor_roster,
777 removal,
778 remaining_roster_state_hash,
779 owner_pubkey,
780 signature: String::new(),
781 };
782 if !intent.verify_shape() {
783 return Err(CircleTransitionError::InvalidCurrentState);
784 }
785 intent.signature = keys::sign_hex(signer, &intent.canonical_bytes()).1;
786 Ok(intent)
787 }
788
789 fn canonical_bytes(&self) -> Vec<u8> {
790 #[derive(Serialize)]
791 struct Signed<'a> {
792 domain: &'static str,
793 version: u32,
794 store_root_hash: ObjectHash,
795 circle_id: CircleId,
796 close_id: CircleEpochCloseId,
797 epoch_id: CircleEpochId,
798 predecessor_roster: &'a MergeCircleRosterStateRef,
799 removal: &'a CircleRosterEntry,
800 remaining_roster_state_hash: ObjectHash,
801 owner_pubkey: &'a str,
802 }
803 serde_json::to_vec(&Signed {
804 domain: "coven.circle-epoch-close-intent.v1",
805 version: self.version,
806 store_root_hash: self.store_root_hash,
807 circle_id: self.circle_id,
808 close_id: self.close_id,
809 epoch_id: self.epoch_id,
810 predecessor_roster: &self.predecessor_roster,
811 removal: &self.removal,
812 remaining_roster_state_hash: self.remaining_roster_state_hash,
813 owner_pubkey: &self.owner_pubkey,
814 })
815 .expect("Circle epoch-close intent serialization cannot fail")
816 }
817
818 fn verify_shape(&self) -> bool {
819 self.version == STORE_PROTOCOL_VERSION
820 && self.removal.verify()
821 && self.removal.store_root_hash == self.store_root_hash
822 && self.removal.circle_id == self.circle_id
823 && self.removal.author_pubkey == self.owner_pubkey
824 && matches!(
825 self.removal.change,
826 super::circle_roster::CircleRosterChange::RemoveMember { .. }
827 )
828 }
829
830 pub fn verify(&self) -> bool {
831 self.verify_shape()
832 && keys::verify_signature_hex(
833 &self.owner_pubkey,
834 &self.signature,
835 &self.canonical_bytes(),
836 )
837 }
838
839 pub fn intent_hash(&self) -> ObjectHash {
840 ObjectHash::digest(
841 &serde_json::to_vec(self).expect("Circle epoch-close intent serialization cannot fail"),
842 )
843 }
844}
845
846#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
847#[serde(deny_unknown_fields)]
848pub struct CircleEpochCloseIntentRef {
849 pub close_id: CircleEpochCloseId,
850 pub intent_hash: ObjectHash,
851 pub object: ExactObjectRef,
852}
853
854impl CircleEpochCloseIntentRef {
855 pub(crate) fn from_intent(
856 intent: &CircleEpochCloseIntent,
857 object: ExactObjectRef,
858 ) -> Result<Self, CircleTransitionError> {
859 let reference = Self {
860 close_id: intent.close_id,
861 intent_hash: intent.intent_hash(),
862 object,
863 };
864 if reference.object.slot().logical_key()
865 != format!(
866 "{}.json",
867 circle_epoch_close_intent_semantic_prefix(
868 intent.circle_id,
869 intent.close_id,
870 intent.intent_hash(),
871 )
872 )
873 {
874 return Err(CircleTransitionError::InvalidCurrentState);
875 }
876 Ok(reference)
877 }
878}
879
880#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
881#[serde(deny_unknown_fields)]
882pub struct CircleEpochClose {
883 pub close_id: CircleEpochCloseId,
884 pub frozen_epoch: MergeActiveCircleEpoch,
885 pub intent: CircleEpochCloseIntentRef,
886 pub frozen_device_state: StoreDeviceStateRef,
887 pub participants: Vec<CircleEpochCloseParticipant>,
888 pub provisional_frontier: CommitFrontier,
889 pub outcome_slot: ObjectSlot,
890}
891
892impl CircleEpochClose {
893 fn verify_shape(&self, circle_id: CircleId) -> bool {
894 super::store_commit::validate_commit_frontier(&self.provisional_frontier).is_ok()
895 && self.intent.close_id == self.close_id
896 && !self.participants.is_empty()
897 && self
898 .participants
899 .windows(2)
900 .all(|pair| pair[0].registration.device_id < pair[1].registration.device_id)
901 && self.participants.iter().all(|participant| {
902 participant.response_slot.logical_key()
903 == format!(
904 "{}.json",
905 circle_epoch_close_response_semantic_prefix(
906 circle_id,
907 self.close_id,
908 participant.registration.device_id,
909 )
910 )
911 })
912 && self.outcome_slot.logical_key()
913 == format!(
914 "{}.json",
915 circle_epoch_close_outcome_semantic_prefix(circle_id, self.close_id)
916 )
917 }
918}
919
920#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
921#[serde(deny_unknown_fields)]
922pub struct CircleEpochCloseResponse {
923 pub version: u32,
924 pub store_root_hash: ObjectHash,
925 pub circle_id: CircleId,
926 pub close_id: CircleEpochCloseId,
927 pub close_control: CircleControlCoord,
928 pub registration: StoreDeviceRegistrationRef,
929 pub frontier: CommitFrontier,
930 pub signature: String,
931}
932
933impl CircleEpochCloseResponse {
934 pub(crate) fn signed(
935 control: &PreparedCircleControl,
936 registration: StoreDeviceRegistrationRef,
937 frontier: CommitFrontier,
938 author: &StoreDeviceRegistration,
939 signer: &UserKeypair,
940 ) -> Result<Self, CircleTransitionError> {
941 let CircleControlState::EpochClose(close) = control.value.state() else {
942 return Err(CircleTransitionError::InvalidCurrentState);
943 };
944 let mut response = Self {
945 version: STORE_PROTOCOL_VERSION,
946 store_root_hash: control.value.store_root_hash,
947 circle_id: control.value.circle_id,
948 close_id: close.close_id,
949 close_control: control.coord.clone(),
950 registration,
951 frontier,
952 signature: String::new(),
953 };
954 response.signature = keys::sign_hex(signer, &response.canonical_bytes()).1;
955 if !response.verify_for(control, author) {
956 return Err(CircleTransitionError::InvalidCurrentState);
957 }
958 Ok(response)
959 }
960
961 fn canonical_bytes(&self) -> Vec<u8> {
962 #[derive(Serialize)]
963 struct Signed<'a> {
964 domain: &'static str,
965 version: u32,
966 store_root_hash: ObjectHash,
967 circle_id: CircleId,
968 close_id: CircleEpochCloseId,
969 close_control: &'a CircleControlCoord,
970 registration: &'a StoreDeviceRegistrationRef,
971 frontier: &'a CommitFrontier,
972 }
973 serde_json::to_vec(&Signed {
974 domain: "coven.circle-epoch-close-response.v1",
975 version: self.version,
976 store_root_hash: self.store_root_hash,
977 circle_id: self.circle_id,
978 close_id: self.close_id,
979 close_control: &self.close_control,
980 registration: &self.registration,
981 frontier: &self.frontier,
982 })
983 .expect("Circle epoch-close response serialization cannot fail")
984 }
985
986 pub(crate) fn verify_for(
987 &self,
988 control: &PreparedCircleControl,
989 author: &StoreDeviceRegistration,
990 ) -> bool {
991 let CircleControlState::EpochClose(close) = control.value.state() else {
992 return false;
993 };
994 self.version == STORE_PROTOCOL_VERSION
995 && control.verify()
996 && self.store_root_hash == control.value.store_root_hash
997 && self.circle_id == control.value.circle_id
998 && self.close_id == close.close_id
999 && self.close_control == control.coord
1000 && super::store_commit::validate_commit_frontier(&self.frontier).is_ok()
1001 && self.frontier.covers(&close.provisional_frontier)
1002 && self.registration.verify_registration(author).is_ok()
1003 && author.store_root.store_root_hash == self.store_root_hash
1004 && close
1005 .participants
1006 .iter()
1007 .any(|participant| participant.registration == self.registration)
1008 && keys::verify_signature_hex(
1009 &author.device_signing_pubkey,
1010 &self.signature,
1011 &self.canonical_bytes(),
1012 )
1013 }
1014
1015 pub fn response_hash(&self) -> ObjectHash {
1016 ObjectHash::digest(
1017 &serde_json::to_vec(self)
1018 .expect("Circle epoch-close response serialization cannot fail"),
1019 )
1020 }
1021}
1022
1023#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1024#[serde(deny_unknown_fields)]
1025pub struct CircleEpochCloseResponseRef {
1026 pub registration: StoreDeviceRegistrationRef,
1027 pub frontier: CommitFrontier,
1028 pub response_hash: ObjectHash,
1029 pub object: ExactObjectRef,
1030}
1031
1032impl CircleEpochCloseResponseRef {
1033 pub(crate) fn from_response(
1034 response: &CircleEpochCloseResponse,
1035 object: ExactObjectRef,
1036 ) -> Result<Self, CircleTransitionError> {
1037 if object.slot().logical_key()
1038 != format!(
1039 "{}.json",
1040 circle_epoch_close_response_semantic_prefix(
1041 response.circle_id,
1042 response.close_id,
1043 response.registration.device_id,
1044 )
1045 )
1046 {
1047 return Err(CircleTransitionError::InvalidCurrentState);
1048 }
1049 Ok(Self {
1050 registration: response.registration.clone(),
1051 frontier: response.frontier.clone(),
1052 response_hash: response.response_hash(),
1053 object,
1054 })
1055 }
1056
1057 pub(crate) fn verify_response(&self, response: &CircleEpochCloseResponse) -> bool {
1058 self.registration == response.registration
1059 && self.frontier == response.frontier
1060 && self.response_hash == response.response_hash()
1061 }
1062}
1063
1064#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1068#[serde(deny_unknown_fields)]
1069pub struct CircleEpochCloseExclusion {
1070 pub version: u32,
1071 pub store_root_hash: ObjectHash,
1072 pub circle_id: CircleId,
1073 pub close_id: CircleEpochCloseId,
1074 pub close_control: CircleControlCoord,
1075 pub excluded: StoreDeviceRegistrationRef,
1076 pub owner_pubkey: String,
1077 pub signature: String,
1078}
1079
1080impl CircleEpochCloseExclusion {
1081 pub(crate) fn signed(
1082 control: &PreparedCircleControl,
1083 excluded: StoreDeviceRegistrationRef,
1084 signer: &UserKeypair,
1085 ) -> Result<Self, CircleTransitionError> {
1086 let CircleControlState::EpochClose(close) = control.value.state() else {
1087 return Err(CircleTransitionError::InvalidCurrentState);
1088 };
1089 let mut exclusion = Self {
1090 version: STORE_PROTOCOL_VERSION,
1091 store_root_hash: control.value.store_root_hash,
1092 circle_id: control.value.circle_id,
1093 close_id: close.close_id,
1094 close_control: control.coord.clone(),
1095 excluded,
1096 owner_pubkey: keys::public_key_hex(signer),
1097 signature: String::new(),
1098 };
1099 if !exclusion.verify_shape(control) {
1100 return Err(CircleTransitionError::InvalidCurrentState);
1101 }
1102 exclusion.signature = keys::sign_hex(signer, &exclusion.canonical_bytes()).1;
1103 Ok(exclusion)
1104 }
1105
1106 fn canonical_bytes(&self) -> Vec<u8> {
1107 #[derive(Serialize)]
1108 struct Signed<'a> {
1109 domain: &'static str,
1110 version: u32,
1111 store_root_hash: ObjectHash,
1112 circle_id: CircleId,
1113 close_id: CircleEpochCloseId,
1114 close_control: &'a CircleControlCoord,
1115 excluded: &'a StoreDeviceRegistrationRef,
1116 owner_pubkey: &'a str,
1117 }
1118 serde_json::to_vec(&Signed {
1119 domain: "coven.circle-epoch-close-exclusion.v1",
1120 version: self.version,
1121 store_root_hash: self.store_root_hash,
1122 circle_id: self.circle_id,
1123 close_id: self.close_id,
1124 close_control: &self.close_control,
1125 excluded: &self.excluded,
1126 owner_pubkey: &self.owner_pubkey,
1127 })
1128 .expect("Circle epoch-close exclusion serialization cannot fail")
1129 }
1130
1131 fn verify_shape(&self, control: &PreparedCircleControl) -> bool {
1132 let CircleControlState::EpochClose(close) = control.value.state() else {
1133 return false;
1134 };
1135 self.version == STORE_PROTOCOL_VERSION
1136 && control.verify()
1137 && self.store_root_hash == control.value.store_root_hash
1138 && self.circle_id == control.value.circle_id
1139 && self.close_id == close.close_id
1140 && self.close_control == control.coord
1141 && close
1142 .participants
1143 .iter()
1144 .any(|participant| participant.registration == self.excluded)
1145 && close
1146 .frozen_epoch
1147 .common
1148 .owners
1149 .contains(&self.owner_pubkey)
1150 }
1151
1152 pub(crate) fn verify_for(&self, control: &PreparedCircleControl) -> bool {
1153 self.verify_shape(control)
1154 && keys::verify_signature_hex(
1155 &self.owner_pubkey,
1156 &self.signature,
1157 &self.canonical_bytes(),
1158 )
1159 }
1160
1161 pub fn exclusion_hash(&self) -> ObjectHash {
1162 ObjectHash::digest(
1163 &serde_json::to_vec(self)
1164 .expect("Circle epoch-close exclusion serialization cannot fail"),
1165 )
1166 }
1167}
1168
1169#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1170#[serde(deny_unknown_fields)]
1171pub struct CircleEpochCloseExclusionRef {
1172 pub registration: StoreDeviceRegistrationRef,
1173 pub exclusion_hash: ObjectHash,
1174 pub object: ExactObjectRef,
1175}
1176
1177impl CircleEpochCloseExclusionRef {
1178 pub(crate) fn from_exclusion(
1179 exclusion: &CircleEpochCloseExclusion,
1180 object: ExactObjectRef,
1181 ) -> Result<Self, CircleTransitionError> {
1182 if object.slot().logical_key()
1183 != format!(
1184 "{}.json",
1185 circle_epoch_close_response_semantic_prefix(
1186 exclusion.circle_id,
1187 exclusion.close_id,
1188 exclusion.excluded.device_id,
1189 )
1190 )
1191 {
1192 return Err(CircleTransitionError::InvalidCurrentState);
1193 }
1194 Ok(Self {
1195 registration: exclusion.excluded.clone(),
1196 exclusion_hash: exclusion.exclusion_hash(),
1197 object,
1198 })
1199 }
1200
1201 pub(crate) fn verify_exclusion(&self, exclusion: &CircleEpochCloseExclusion) -> bool {
1202 self.registration == exclusion.excluded && self.exclusion_hash == exclusion.exclusion_hash()
1203 }
1204}
1205
1206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1209#[serde(rename_all = "snake_case", deny_unknown_fields)]
1210pub enum CircleEpochCloseResponseSlotValue {
1211 Response(CircleEpochCloseResponse),
1212 Exclusion(CircleEpochCloseExclusion),
1213}
1214
1215impl CircleEpochCloseResponseSlotValue {
1216 pub(crate) fn to_bytes(&self) -> Vec<u8> {
1217 serde_json::to_vec(self)
1218 .expect("Circle epoch-close response slot value serialization cannot fail")
1219 }
1220
1221 pub(crate) fn parse(bytes: &[u8]) -> Result<Self, CircleTransitionError> {
1222 let value: Self = serde_json::from_slice(bytes)
1223 .map_err(|_| CircleTransitionError::InvalidCurrentState)?;
1224 if value.to_bytes() != bytes {
1225 return Err(CircleTransitionError::InvalidCurrentState);
1226 }
1227 Ok(value)
1228 }
1229}
1230
1231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1235#[serde(rename_all = "snake_case", deny_unknown_fields)]
1236pub enum CircleEpochCloseSettlement {
1237 Response(CircleEpochCloseResponseRef),
1238 Exclusion(CircleEpochCloseExclusionRef),
1239}
1240
1241impl CircleEpochCloseSettlement {
1242 pub(crate) fn registration(&self) -> &StoreDeviceRegistrationRef {
1243 match self {
1244 Self::Response(reference) => &reference.registration,
1245 Self::Exclusion(reference) => &reference.registration,
1246 }
1247 }
1248
1249 pub(crate) fn object(&self) -> &ExactObjectRef {
1250 match self {
1251 Self::Response(reference) => &reference.object,
1252 Self::Exclusion(reference) => &reference.object,
1253 }
1254 }
1255
1256 pub(crate) fn response_frontier(&self) -> Option<&CommitFrontier> {
1257 match self {
1258 Self::Response(reference) => Some(&reference.frontier),
1259 Self::Exclusion(_) => None,
1260 }
1261 }
1262}
1263
1264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1265#[serde(deny_unknown_fields)]
1266pub struct CircleEpochSuccessor {
1267 pub epoch_id: CircleEpochId,
1268 pub key_fingerprint: KeyFingerprint,
1269 pub owners: Vec<String>,
1270 pub access_root: ObjectHash,
1271 pub metadata: MergeCircleMetadataStateRef,
1272 pub roster: MergeCircleRosterStateRef,
1273 pub store_membership: StoreMembershipStateRef,
1274}
1275
1276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1277#[serde(deny_unknown_fields)]
1278pub struct CircleEpochCloseOutcome {
1279 pub version: u32,
1280 pub store_root_hash: ObjectHash,
1281 pub circle_id: CircleId,
1282 pub close_id: CircleEpochCloseId,
1283 pub close_control: CircleControlCoord,
1284 pub intent: CircleEpochCloseIntentRef,
1285 pub responses: Vec<CircleEpochCloseSettlement>,
1286 pub cutoff: CommitFrontier,
1287 pub successor: CircleEpochSuccessor,
1288 pub owner_pubkey: String,
1289 pub signature: String,
1290}
1291
1292impl CircleEpochCloseOutcome {
1293 pub(crate) fn signed(
1294 control: &PreparedCircleControl,
1295 intent: &CircleEpochCloseIntent,
1296 responses: Vec<CircleEpochCloseSettlement>,
1297 successor: CircleEpochSuccessor,
1298 signer: &UserKeypair,
1299 ) -> Result<Self, CircleTransitionError> {
1300 let CircleControlState::EpochClose(close) = control.value.state() else {
1301 return Err(CircleTransitionError::InvalidCurrentState);
1302 };
1303 let cutoff = responses
1304 .iter()
1305 .filter_map(CircleEpochCloseSettlement::response_frontier)
1306 .try_fold(close.provisional_frontier.clone(), |cutoff, frontier| {
1307 cutoff.join(frontier.clone())
1308 })
1309 .map_err(|_| CircleTransitionError::InvalidCurrentState)?;
1310 let mut outcome = Self {
1311 version: STORE_PROTOCOL_VERSION,
1312 store_root_hash: control.value.store_root_hash,
1313 circle_id: control.value.circle_id,
1314 close_id: close.close_id,
1315 close_control: control.coord.clone(),
1316 intent: close.intent.clone(),
1317 responses,
1318 cutoff,
1319 successor,
1320 owner_pubkey: keys::public_key_hex(signer),
1321 signature: String::new(),
1322 };
1323 if !outcome.verify_shape(control) || !outcome.verify_intent(intent) {
1324 return Err(CircleTransitionError::InvalidCurrentState);
1325 }
1326 outcome.signature = keys::sign_hex(signer, &outcome.canonical_bytes()).1;
1327 Ok(outcome)
1328 }
1329
1330 fn canonical_bytes(&self) -> Vec<u8> {
1331 #[derive(Serialize)]
1332 struct Signed<'a> {
1333 domain: &'static str,
1334 version: u32,
1335 store_root_hash: ObjectHash,
1336 circle_id: CircleId,
1337 close_id: CircleEpochCloseId,
1338 close_control: &'a CircleControlCoord,
1339 intent: &'a CircleEpochCloseIntentRef,
1340 responses: &'a [CircleEpochCloseSettlement],
1341 cutoff: &'a CommitFrontier,
1342 successor: &'a CircleEpochSuccessor,
1343 owner_pubkey: &'a str,
1344 }
1345 serde_json::to_vec(&Signed {
1346 domain: "coven.circle-epoch-close-outcome.v1",
1347 version: self.version,
1348 store_root_hash: self.store_root_hash,
1349 circle_id: self.circle_id,
1350 close_id: self.close_id,
1351 close_control: &self.close_control,
1352 intent: &self.intent,
1353 responses: &self.responses,
1354 cutoff: &self.cutoff,
1355 successor: &self.successor,
1356 owner_pubkey: &self.owner_pubkey,
1357 })
1358 .expect("Circle epoch-close outcome serialization cannot fail")
1359 }
1360
1361 fn verify_shape(&self, control: &PreparedCircleControl) -> bool {
1362 let CircleControlState::EpochClose(close) = control.value.state() else {
1363 return false;
1364 };
1365 let responses_are_canonical = self
1366 .responses
1367 .windows(2)
1368 .all(|pair| pair[0].registration().device_id < pair[1].registration().device_id);
1369 let responses_match_participants =
1370 self.responses.len() == close.participants.len()
1371 && self.responses.iter().zip(&close.participants).all(
1372 |(settlement, participant)| {
1373 settlement.registration() == &participant.registration
1374 && settlement.object().slot() == &participant.response_slot
1375 && settlement
1376 .response_frontier()
1377 .is_none_or(|frontier| frontier.covers(&close.provisional_frontier))
1378 },
1379 );
1380 let expected_cutoff = self
1381 .responses
1382 .iter()
1383 .filter_map(CircleEpochCloseSettlement::response_frontier)
1384 .try_fold(close.provisional_frontier.clone(), |cutoff, frontier| {
1385 cutoff.join(frontier.clone())
1386 });
1387 self.version == STORE_PROTOCOL_VERSION
1388 && control.verify()
1389 && self.store_root_hash == control.value.store_root_hash
1390 && self.circle_id == control.value.circle_id
1391 && self.close_id == close.close_id
1392 && self.close_control == control.coord
1393 && self.intent == close.intent
1394 && responses_are_canonical
1395 && responses_match_participants
1396 && expected_cutoff.is_ok_and(|cutoff| cutoff == self.cutoff)
1397 && super::store_commit::validate_commit_frontier(&self.cutoff).is_ok()
1398 && self.successor.epoch_id != close.frozen_epoch.common.epoch_id
1399 && self.successor.key_fingerprint != close.frozen_epoch.common.key_fingerprint
1400 && !self.successor.owners.is_empty()
1401 && self
1402 .successor
1403 .owners
1404 .windows(2)
1405 .all(|pair| pair[0] < pair[1])
1406 && close
1407 .frozen_epoch
1408 .common
1409 .owners
1410 .contains(&self.owner_pubkey)
1411 }
1412
1413 fn verify_intent(&self, intent: &CircleEpochCloseIntent) -> bool {
1414 intent.verify()
1415 && intent.close_id == self.close_id
1416 && intent.circle_id == self.circle_id
1417 && intent.store_root_hash == self.store_root_hash
1418 && intent.intent_hash() == self.intent.intent_hash
1419 && self.successor.roster.state_hash == intent.remaining_roster_state_hash
1420 }
1421
1422 pub(crate) fn verify_for(
1423 &self,
1424 control: &PreparedCircleControl,
1425 intent: &CircleEpochCloseIntent,
1426 settlements: &[(
1427 CircleEpochCloseSettlement,
1428 CircleEpochCloseResponseSlotValue,
1429 )],
1430 ) -> bool {
1431 self.verify_shape(control)
1432 && self.verify_intent(intent)
1433 && self.responses.len() == settlements.len()
1434 && self
1435 .responses
1436 .iter()
1437 .zip(settlements)
1438 .all(|(expected, (settlement, slot_value))| {
1439 expected == settlement
1440 && match (settlement, slot_value) {
1441 (
1442 CircleEpochCloseSettlement::Response(reference),
1443 CircleEpochCloseResponseSlotValue::Response(response),
1444 ) => {
1445 reference.verify_response(response)
1446 && response.close_control == self.close_control
1447 }
1448 (
1449 CircleEpochCloseSettlement::Exclusion(reference),
1450 CircleEpochCloseResponseSlotValue::Exclusion(exclusion),
1451 ) => {
1452 reference.verify_exclusion(exclusion)
1453 && exclusion.verify_for(control)
1454 && exclusion.close_control == self.close_control
1455 }
1456 _ => false,
1457 }
1458 })
1459 && keys::verify_signature_hex(
1460 &self.owner_pubkey,
1461 &self.signature,
1462 &self.canonical_bytes(),
1463 )
1464 }
1465
1466 pub(crate) fn verify_signature(&self) -> bool {
1467 keys::verify_signature_hex(&self.owner_pubkey, &self.signature, &self.canonical_bytes())
1468 }
1469
1470 pub fn outcome_hash(&self) -> ObjectHash {
1471 ObjectHash::digest(
1472 &serde_json::to_vec(self)
1473 .expect("Circle epoch-close outcome serialization cannot fail"),
1474 )
1475 }
1476}
1477
1478#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1479#[serde(deny_unknown_fields)]
1480pub struct CircleEpochCloseOutcomeRef {
1481 pub close_id: CircleEpochCloseId,
1482 pub outcome_hash: ObjectHash,
1483 pub object: ExactObjectRef,
1484}
1485
1486impl CircleEpochCloseOutcomeRef {
1487 pub(crate) fn from_outcome(
1488 outcome: &CircleEpochCloseOutcome,
1489 object: ExactObjectRef,
1490 ) -> Result<Self, CircleTransitionError> {
1491 if object.slot().logical_key()
1492 != format!(
1493 "{}.json",
1494 circle_epoch_close_outcome_semantic_prefix(outcome.circle_id, outcome.close_id)
1495 )
1496 {
1497 return Err(CircleTransitionError::InvalidCurrentState);
1498 }
1499 Ok(Self {
1500 close_id: outcome.close_id,
1501 outcome_hash: outcome.outcome_hash(),
1502 object,
1503 })
1504 }
1505}
1506
1507#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1511#[serde(deny_unknown_fields)]
1512pub struct CircleEpochCloseCancellation {
1513 pub version: u32,
1514 pub store_root_hash: ObjectHash,
1515 pub circle_id: CircleId,
1516 pub close_id: CircleEpochCloseId,
1517 pub close_control: CircleControlCoord,
1518 pub intent: CircleEpochCloseIntentRef,
1519 pub owner_pubkey: String,
1520 pub signature: String,
1521}
1522
1523impl CircleEpochCloseCancellation {
1524 pub(crate) fn signed(
1525 control: &PreparedCircleControl,
1526 signer: &UserKeypair,
1527 ) -> Result<Self, CircleTransitionError> {
1528 let CircleControlState::EpochClose(close) = control.value.state() else {
1529 return Err(CircleTransitionError::InvalidCurrentState);
1530 };
1531 let mut cancellation = Self {
1532 version: STORE_PROTOCOL_VERSION,
1533 store_root_hash: control.value.store_root_hash,
1534 circle_id: control.value.circle_id,
1535 close_id: close.close_id,
1536 close_control: control.coord.clone(),
1537 intent: close.intent.clone(),
1538 owner_pubkey: keys::public_key_hex(signer),
1539 signature: String::new(),
1540 };
1541 if !cancellation.verify_shape(control) {
1542 return Err(CircleTransitionError::InvalidCurrentState);
1543 }
1544 cancellation.signature = keys::sign_hex(signer, &cancellation.canonical_bytes()).1;
1545 Ok(cancellation)
1546 }
1547
1548 fn canonical_bytes(&self) -> Vec<u8> {
1549 #[derive(Serialize)]
1550 struct Signed<'a> {
1551 domain: &'static str,
1552 version: u32,
1553 store_root_hash: ObjectHash,
1554 circle_id: CircleId,
1555 close_id: CircleEpochCloseId,
1556 close_control: &'a CircleControlCoord,
1557 intent: &'a CircleEpochCloseIntentRef,
1558 owner_pubkey: &'a str,
1559 }
1560 serde_json::to_vec(&Signed {
1561 domain: "coven.circle-epoch-close-cancellation.v1",
1562 version: self.version,
1563 store_root_hash: self.store_root_hash,
1564 circle_id: self.circle_id,
1565 close_id: self.close_id,
1566 close_control: &self.close_control,
1567 intent: &self.intent,
1568 owner_pubkey: &self.owner_pubkey,
1569 })
1570 .expect("Circle epoch-close cancellation serialization cannot fail")
1571 }
1572
1573 fn verify_shape(&self, control: &PreparedCircleControl) -> bool {
1574 let CircleControlState::EpochClose(close) = control.value.state() else {
1575 return false;
1576 };
1577 self.version == STORE_PROTOCOL_VERSION
1578 && control.verify()
1579 && self.store_root_hash == control.value.store_root_hash
1580 && self.circle_id == control.value.circle_id
1581 && self.close_id == close.close_id
1582 && self.close_control == control.coord
1583 && self.intent == close.intent
1584 && close
1585 .frozen_epoch
1586 .common
1587 .owners
1588 .contains(&self.owner_pubkey)
1589 }
1590
1591 pub(crate) fn verify_for(&self, control: &PreparedCircleControl) -> bool {
1592 self.verify_shape(control)
1593 && keys::verify_signature_hex(
1594 &self.owner_pubkey,
1595 &self.signature,
1596 &self.canonical_bytes(),
1597 )
1598 }
1599
1600 pub(crate) fn verify_signature(&self) -> bool {
1601 keys::verify_signature_hex(&self.owner_pubkey, &self.signature, &self.canonical_bytes())
1602 }
1603
1604 pub fn cancellation_hash(&self) -> ObjectHash {
1605 ObjectHash::digest(
1606 &serde_json::to_vec(self)
1607 .expect("Circle epoch-close cancellation serialization cannot fail"),
1608 )
1609 }
1610}
1611
1612#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1613#[serde(deny_unknown_fields)]
1614pub struct CircleEpochCloseCancellationRef {
1615 pub close_id: CircleEpochCloseId,
1616 pub cancellation_hash: ObjectHash,
1617 pub object: ExactObjectRef,
1618}
1619
1620impl CircleEpochCloseCancellationRef {
1621 pub(crate) fn from_cancellation(
1622 cancellation: &CircleEpochCloseCancellation,
1623 object: ExactObjectRef,
1624 ) -> Result<Self, CircleTransitionError> {
1625 if object.slot().logical_key()
1626 != format!(
1627 "{}.json",
1628 circle_epoch_close_outcome_semantic_prefix(
1629 cancellation.circle_id,
1630 cancellation.close_id
1631 )
1632 )
1633 {
1634 return Err(CircleTransitionError::InvalidCurrentState);
1635 }
1636 Ok(Self {
1637 close_id: cancellation.close_id,
1638 cancellation_hash: cancellation.cancellation_hash(),
1639 object,
1640 })
1641 }
1642}
1643
1644#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1648#[serde(rename_all = "snake_case", deny_unknown_fields)]
1649pub enum CircleEpochCloseSlotValue {
1650 Outcome(CircleEpochCloseOutcome),
1651 Cancellation(CircleEpochCloseCancellation),
1652}
1653
1654impl CircleEpochCloseSlotValue {
1655 pub(crate) fn to_bytes(&self) -> Vec<u8> {
1656 serde_json::to_vec(self).expect("Circle epoch-close slot value serialization cannot fail")
1657 }
1658
1659 pub(crate) fn parse(bytes: &[u8]) -> Result<Self, CircleTransitionError> {
1660 let value: Self = serde_json::from_slice(bytes)
1661 .map_err(|_| CircleTransitionError::InvalidCurrentState)?;
1662 if value.to_bytes() != bytes {
1663 return Err(CircleTransitionError::InvalidCurrentState);
1664 }
1665 Ok(value)
1666 }
1667}
1668
1669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1674#[serde(deny_unknown_fields)]
1675pub struct DeletedCircle {
1676 pub frozen_epoch: MergeActiveCircleEpoch,
1677}
1678
1679#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1680#[serde(rename_all = "snake_case", deny_unknown_fields)]
1681pub enum CircleControlState {
1682 ActiveEpoch(MergeActiveCircleEpoch),
1683 EpochClose(CircleEpochClose),
1684 Deleted(DeletedCircle),
1685}
1686
1687impl CircleControlState {
1688 pub(crate) fn access_epoch(&self) -> &MergeActiveCircleEpoch {
1689 match self {
1690 Self::ActiveEpoch(active) => active,
1691 Self::EpochClose(close) => &close.frozen_epoch,
1692 Self::Deleted(deleted) => &deleted.frozen_epoch,
1693 }
1694 }
1695
1696 pub(crate) fn access_epoch_mut(&mut self) -> &mut MergeActiveCircleEpoch {
1697 match self {
1698 Self::ActiveEpoch(active) => active,
1699 Self::EpochClose(close) => &mut close.frozen_epoch,
1700 Self::Deleted(deleted) => &mut deleted.frozen_epoch,
1701 }
1702 }
1703
1704 pub(crate) fn active_epoch(&self) -> Option<&MergeActiveCircleEpoch> {
1705 match self {
1706 Self::ActiveEpoch(active) => Some(active),
1707 Self::EpochClose(_) | Self::Deleted(_) => None,
1708 }
1709 }
1710
1711 pub(crate) fn active_epoch_mut(&mut self) -> Option<&mut MergeActiveCircleEpoch> {
1712 match self {
1713 Self::ActiveEpoch(active) => Some(active),
1714 Self::EpochClose(_) | Self::Deleted(_) => None,
1715 }
1716 }
1717
1718 pub(crate) fn is_deleted(&self) -> bool {
1719 matches!(self, Self::Deleted(_))
1720 }
1721}
1722
1723#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1724#[serde(deny_unknown_fields)]
1725pub struct MergeCircleControlHeadRef {
1726 pub coord: CircleControlCoord,
1727 pub head_hash: ObjectHash,
1728 pub object: ExactObjectRef,
1729}
1730
1731#[derive(Debug, Clone)]
1739pub(crate) struct ResolvedConflictBranch {
1740 pub control_head: MergeCircleControlHeadRef,
1741 pub metadata_heads: Vec<CircleMetadataHeadRef>,
1742 pub roster_heads: Vec<CircleRosterHeadRef>,
1743 pub selected_metadata: CircleMetadata,
1744}
1745
1746pub(crate) fn merge_frontier_head<H>(
1752 frontier: &mut Vec<H>,
1753 head: H,
1754 stream_key: impl Fn(&H) -> CircleAuthorStreamKey,
1755 seq: impl Fn(&H) -> u64,
1756) {
1757 let key = stream_key(&head);
1758 match frontier
1759 .iter_mut()
1760 .find(|existing| stream_key(existing) == key)
1761 {
1762 Some(existing) if seq(&head) > seq(existing) => *existing = head,
1763 Some(_) => {}
1764 None => frontier.push(head),
1765 }
1766}
1767
1768#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1769#[serde(rename_all = "snake_case", deny_unknown_fields)]
1770pub enum MergeCircleOwnerAuthorityRef {
1771 Roster {
1772 roster: MergeCircleRosterStateRef,
1773 grant_id: MembershipGrantId,
1774 created_at: super::circle_roster::CircleRosterCoord,
1775 },
1776 ConflictResolution {
1777 conflict_hash: ObjectHash,
1778 resolution_hash: ObjectHash,
1779 },
1780}
1781
1782impl MergeCircleOwnerAuthorityRef {
1783 pub fn grant_id(&self, author_pubkey: &str) -> MembershipGrantId {
1784 match self {
1785 Self::Roster { grant_id, .. } => grant_id.clone(),
1786 Self::ConflictResolution { conflict_hash, .. } => {
1787 super::circle_roster::derive_circle_resolution_grant(conflict_hash, author_pubkey)
1788 }
1789 }
1790 }
1791}
1792
1793#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1794#[serde(deny_unknown_fields)]
1795pub struct CircleControlValue {
1796 pub order: MergeCircleControlOrder,
1797 pub state: CircleControlState,
1798 pub author_authority: MergeCircleOwnerAuthorityRef,
1799 pub membership_authority: MembershipGrantCreationAuthority,
1800}
1801
1802#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1803#[serde(deny_unknown_fields)]
1804pub struct CircleControl {
1805 pub version: u32,
1806 pub store_root_hash: ObjectHash,
1807 pub circle_id: CircleId,
1808 pub value: CircleControlValue,
1809 pub author_pubkey: String,
1810 pub signature: String,
1811}
1812
1813impl CircleControl {
1814 pub fn state(&self) -> &CircleControlState {
1815 &self.value.state
1816 }
1817
1818 pub fn active_epoch(&self) -> Option<&MergeActiveCircleEpoch> {
1819 self.value.state.active_epoch()
1820 }
1821
1822 pub(crate) fn access_epoch(&self) -> &MergeActiveCircleEpoch {
1823 self.value.state.access_epoch()
1824 }
1825
1826 pub fn active_common(&self) -> &ActiveCircleEpochCore {
1827 &self.access_epoch().common
1828 }
1829
1830 pub fn epoch_id(&self) -> CircleEpochId {
1831 self.active_common().epoch_id
1832 }
1833
1834 pub fn key_fingerprint(&self) -> KeyFingerprint {
1835 self.active_common().key_fingerprint
1836 }
1837
1838 pub fn owners(&self) -> &[String] {
1839 &self.active_common().owners
1840 }
1841
1842 pub fn access_root(&self) -> ObjectHash {
1843 self.active_common().access_root
1844 }
1845
1846 pub fn roster_state_ref(&self) -> CircleRosterStateRef {
1847 self.access_epoch().roster.clone()
1848 }
1849
1850 pub fn metadata_state_ref(&self) -> CircleMetadataStateRef {
1851 self.access_epoch().metadata.clone()
1852 }
1853
1854 pub fn store_membership_state_ref(&self) -> StoreMembershipStateRef {
1855 self.access_epoch().store_membership.clone()
1856 }
1857
1858 pub fn membership_authority(&self) -> &MembershipGrantCreationAuthority {
1859 &self.value.membership_authority
1860 }
1861
1862 pub fn previous_control_hash(&self) -> Option<ObjectHash> {
1863 self.value.order.previous_control_hash
1864 }
1865
1866 pub(crate) fn is_founder(&self) -> bool {
1867 self.value.order.seq == 1
1868 && self.value.order.previous_control_hash.is_none()
1869 && self.value.order.dependencies.is_empty()
1870 }
1871
1872 pub(crate) fn causally_covers(&self, prior: &Self) -> bool {
1873 if self.store_root_hash != prior.store_root_hash || self.circle_id != prior.circle_id {
1874 return false;
1875 }
1876 self.value.order.previous_control_hash == Some(prior.control_hash())
1877 || self
1878 .value
1879 .order
1880 .dependencies
1881 .binary_search(&prior.coord())
1882 .is_ok()
1883 }
1884
1885 pub fn ordinal(&self) -> u64 {
1886 self.value.order.seq
1887 }
1888
1889 pub fn author_grant_id(&self) -> MembershipGrantId {
1890 self.value.author_authority.grant_id(&self.author_pubkey)
1891 }
1892
1893 pub(crate) fn canonical_bytes(&self) -> Vec<u8> {
1894 #[derive(Serialize)]
1895 struct Signed<'a> {
1896 domain: &'static str,
1897 version: u32,
1898 store_root_hash: ObjectHash,
1899 circle_id: CircleId,
1900 value: &'a CircleControlValue,
1901 author_pubkey: &'a str,
1902 }
1903 serde_json::to_vec(&Signed {
1904 domain: CONTROL_DOMAIN,
1905 version: self.version,
1906 store_root_hash: self.store_root_hash,
1907 circle_id: self.circle_id,
1908 value: &self.value,
1909 author_pubkey: &self.author_pubkey,
1910 })
1911 .expect("circle control serialization cannot fail")
1912 }
1913
1914 pub fn control_hash(&self) -> ObjectHash {
1915 ObjectHash::digest(
1916 &serde_json::to_vec(self).expect("circle control serialization cannot fail"),
1917 )
1918 }
1919
1920 pub fn verify(&self) -> bool {
1921 let order = &self.value.order;
1922 let access_epoch = self.access_epoch();
1923 let author_authority = &self.value.author_authority;
1924 let grant_id = author_authority.grant_id(&self.author_pubkey);
1925 let stream_key = CircleAuthorStreamKey {
1926 author_pubkey: self.author_pubkey.clone(),
1927 device_id: order.device_id.clone(),
1928 stream_id: order.stream_id,
1929 author_owner_grant: order.author_owner_grant.clone(),
1930 };
1931 let covered_are_canonical = access_epoch
1932 .covered_control_heads
1933 .windows(2)
1934 .all(|pair| pair[0].coord.stream_key() < pair[1].coord.stream_key());
1935 let own_predecessor = access_epoch
1936 .covered_control_heads
1937 .iter()
1938 .find(|head| head.coord.stream_key() == stream_key);
1939 let expected_dependencies = access_epoch
1940 .covered_control_heads
1941 .iter()
1942 .filter(|head| head.coord.stream_key() != stream_key)
1943 .map(|head| head.coord.clone())
1944 .collect::<Vec<_>>();
1945 let order_is_valid = !order.device_id.is_empty()
1946 && order.seq > 0
1947 && order.author_owner_grant == grant_id
1948 && covered_are_canonical
1949 && order.dependencies == expected_dependencies;
1950 let authority_is_founder_roster = matches!(
1951 author_authority,
1952 MergeCircleOwnerAuthorityRef::Roster { roster, .. }
1953 if roster == &access_epoch.roster
1954 );
1955 let founder = order.seq == 1 && access_epoch.covered_control_heads.is_empty();
1956 let continuity_is_valid = match (order.seq, own_predecessor) {
1957 (1, None) => order.previous_control_hash.is_none(),
1958 (seq, Some(predecessor)) if seq > 1 => {
1959 predecessor.coord.seq.checked_add(1) == Some(seq)
1960 && order.previous_control_hash == Some(predecessor.coord.control_hash)
1961 }
1962 _ => false,
1963 };
1964 let founder_identity_is_valid = !founder
1965 || (authority_is_founder_roster
1966 && self.circle_id
1967 == CircleId::founder(self.store_root_hash, &self.author_pubkey, &grant_id));
1968 let common = &access_epoch.common;
1969 let owners_are_canonical =
1970 !common.owners.is_empty() && common.owners.windows(2).all(|pair| pair[0] < pair[1]);
1971 let origin_is_valid = match &common.origin {
1972 CircleEpochOrigin::Founder => true,
1973 CircleEpochOrigin::Closed { cutoff, .. } => {
1974 super::store_commit::validate_commit_frontier(cutoff).is_ok()
1975 }
1976 };
1977 let state_is_valid = match &self.value.state {
1978 CircleControlState::ActiveEpoch(_) => true,
1979 CircleControlState::EpochClose(close) => !founder && close.verify_shape(self.circle_id),
1980 CircleControlState::Deleted(_) => !founder,
1984 };
1985 self.version == STORE_PROTOCOL_VERSION
1986 && owners_are_canonical
1987 && origin_is_valid
1988 && state_is_valid
1989 && order_is_valid
1990 && continuity_is_valid
1991 && founder_identity_is_valid
1992 && keys::verify_signature_hex(
1993 &self.author_pubkey,
1994 &self.signature,
1995 &self.canonical_bytes(),
1996 )
1997 }
1998
1999 pub fn coord(&self) -> CircleControlCoord {
2000 let order = &self.value.order;
2001 CircleControlCoord {
2002 device_id: order.device_id.clone(),
2003 stream_id: order.stream_id,
2004 author_pubkey: self.author_pubkey.clone(),
2005 author_owner_grant: order.author_owner_grant.clone(),
2006 seq: order.seq,
2007 control_hash: self.control_hash(),
2008 }
2009 }
2010}
2011
2012#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2013#[serde(deny_unknown_fields)]
2014pub struct CircleControlHead {
2015 pub version: u32,
2016 pub store_root_hash: ObjectHash,
2017 pub circle_id: CircleId,
2018 pub control: CircleControlCoord,
2019 pub entry: ExactObjectRef,
2020 pub successor: SuccessorLink,
2021 pub signature: String,
2022}
2023
2024impl CircleControlHead {
2025 pub(crate) fn signed(
2026 control: &CircleControl,
2027 entry: ExactObjectRef,
2028 successor: SuccessorLink,
2029 signer: &UserKeypair,
2030 ) -> Self {
2031 let mut head = Self {
2032 version: STORE_PROTOCOL_VERSION,
2033 store_root_hash: control.store_root_hash,
2034 circle_id: control.circle_id,
2035 control: control.coord(),
2036 entry,
2037 successor,
2038 signature: String::new(),
2039 };
2040 head.signature = keys::sign_hex(signer, &head.canonical_bytes()).1;
2041 head
2042 }
2043
2044 pub(crate) fn canonical_bytes(&self) -> Vec<u8> {
2045 #[derive(Serialize)]
2046 struct Signed<'a> {
2047 domain: &'static str,
2048 version: u32,
2049 store_root_hash: ObjectHash,
2050 circle_id: CircleId,
2051 control: &'a CircleControlCoord,
2052 entry: &'a ExactObjectRef,
2053 successor: &'a SuccessorLink,
2054 }
2055 serde_json::to_vec(&Signed {
2056 domain: "coven.circle-control-head.v1",
2057 version: self.version,
2058 store_root_hash: self.store_root_hash,
2059 circle_id: self.circle_id,
2060 control: &self.control,
2061 entry: &self.entry,
2062 successor: &self.successor,
2063 })
2064 .expect("circle control head serialization cannot fail")
2065 }
2066
2067 pub fn head_hash(&self) -> ObjectHash {
2068 ObjectHash::digest(
2069 &serde_json::to_vec(self).expect("circle control head serialization cannot fail"),
2070 )
2071 }
2072
2073 pub fn verify(&self, registration: &StoreDeviceRegistration) -> bool {
2074 self.version == STORE_PROTOCOL_VERSION
2075 && self.control.validate().is_ok()
2076 && self.control.device_id == registration.device_id.to_string()
2077 && keys::verify_signature_hex(
2078 ®istration.device_signing_pubkey,
2079 &self.signature,
2080 &self.canonical_bytes(),
2081 )
2082 }
2083}
2084
2085#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2086#[serde(deny_unknown_fields)]
2087pub struct AccessEnvelope {
2088 pub version: u32,
2089 pub store_root_hash: ObjectHash,
2090 pub candidate_family: super::store_commit::CandidateFamilyId,
2091 pub circle_id: CircleId,
2092 pub owner_pubkey: String,
2093 pub recipient_slot: String,
2094 pub control_hash: ObjectHash,
2095 pub leaf_id: AccessLeafId,
2096 pub leaf_hash: ObjectHash,
2097 pub value_hash: ObjectHash,
2098 pub proof: Vec<MerkleStep>,
2099 pub signature: String,
2100}
2101
2102impl AccessEnvelope {
2103 pub(crate) fn canonical_bytes(&self) -> Vec<u8> {
2104 #[derive(Serialize)]
2105 struct Signed<'a> {
2106 domain: &'static str,
2107 version: u32,
2108 store_root_hash: ObjectHash,
2109 candidate_family: super::store_commit::CandidateFamilyId,
2110 circle_id: CircleId,
2111 owner_pubkey: &'a str,
2112 recipient_slot: &'a str,
2113 control_hash: ObjectHash,
2114 leaf_id: AccessLeafId,
2115 leaf_hash: ObjectHash,
2116 value_hash: ObjectHash,
2117 proof: &'a [MerkleStep],
2118 }
2119 serde_json::to_vec(&Signed {
2120 domain: ENVELOPE_DOMAIN,
2121 version: self.version,
2122 store_root_hash: self.store_root_hash,
2123 candidate_family: self.candidate_family,
2124 circle_id: self.circle_id,
2125 owner_pubkey: &self.owner_pubkey,
2126 recipient_slot: &self.recipient_slot,
2127 control_hash: self.control_hash,
2128 leaf_id: self.leaf_id,
2129 leaf_hash: self.leaf_hash,
2130 value_hash: self.value_hash,
2131 proof: &self.proof,
2132 })
2133 .expect("access envelope serialization cannot fail")
2134 }
2135
2136 pub fn verify(
2137 &self,
2138 control: &PreparedCircleControl,
2139 candidate_family: super::store_commit::CandidateFamilyId,
2140 ) -> bool {
2141 self.version == STORE_PROTOCOL_VERSION
2142 && self.store_root_hash == control.value.store_root_hash
2143 && self.candidate_family == candidate_family
2144 && self.circle_id == control.value.circle_id
2145 && self.owner_pubkey == control.value.author_pubkey
2146 && self.control_hash == control.coord.control_hash()
2147 && keys::verify_signature_hex(
2148 &self.owner_pubkey,
2149 &self.signature,
2150 &self.canonical_bytes(),
2151 )
2152 && verify_merkle_proof(self.leaf_hash, &self.proof, control.value.access_root())
2153 }
2154}
2155
2156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2157#[serde(deny_unknown_fields)]
2158pub struct PreparedCircleControl {
2159 pub coord: CircleControlCoord,
2160 pub bytes: Vec<u8>,
2161 pub value: CircleControl,
2162}
2163
2164impl PreparedCircleControl {
2165 pub fn verify(&self) -> bool {
2166 self.bytes
2167 == serde_json::to_vec(&self.value).expect("circle control serialization cannot fail")
2168 && self.value.verify()
2169 && self.coord == self.value.coord()
2170 }
2171}
2172
2173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2174#[serde(deny_unknown_fields)]
2175pub struct PreparedAccessLeaf {
2176 pub bytes: Vec<u8>,
2177 pub value: CircleAccessLeaf,
2178 pub leaf_hash: ObjectHash,
2179}
2180
2181impl PreparedAccessLeaf {
2182 pub fn verify(
2183 &self,
2184 control: &PreparedCircleControl,
2185 candidate_family: super::store_commit::CandidateFamilyId,
2186 ) -> bool {
2187 self.value.verify_for_control(control, candidate_family)
2188 && ObjectHash::digest(&self.bytes) == self.leaf_hash
2189 }
2190
2191 pub fn verify_envelope(
2192 &self,
2193 control: &PreparedCircleControl,
2194 envelope: &AccessEnvelope,
2195 candidate_family: super::store_commit::CandidateFamilyId,
2196 ) -> bool {
2197 self.verify(control, candidate_family)
2198 && envelope.verify(control, candidate_family)
2199 && self.leaf_hash == envelope.leaf_hash
2200 && envelope.value_hash
2201 == ObjectHash::digest(
2202 &serde_json::to_vec(&self.value)
2203 .expect("circle access leaf serialization cannot fail"),
2204 )
2205 && self.value.leaf_id == envelope.leaf_id
2206 && self.value.owner_pubkey == envelope.owner_pubkey
2207 && self.value.recipient_slot == envelope.recipient_slot
2208 }
2209}
2210
2211#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2212#[serde(deny_unknown_fields)]
2213pub struct PreparedCircleAccess {
2214 pub leaf: PreparedAccessLeaf,
2215 pub envelope: AccessEnvelope,
2216}
2217
2218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2219#[serde(deny_unknown_fields)]
2220pub struct CircleRosterPolicyObjects {
2221 pub entry: CircleRosterEntry,
2222 pub head: CircleRosterHead,
2223}
2224
2225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2226#[serde(deny_unknown_fields)]
2227pub struct CircleTransitionPolicyObjects {
2228 pub roster: Option<CircleRosterPolicyObjects>,
2229 pub metadata_head: Option<CircleMetadataHead>,
2230 pub control_head: CircleControlHead,
2231}
2232
2233#[derive(Debug, Clone)]
2234pub(crate) enum CircleRosterDraftPolicy {
2235 Inherited,
2236 Founder {
2237 entry: CircleRosterEntry,
2238 },
2239 Successor {
2240 predecessor: CircleRosterChain,
2241 entry: CircleRosterEntry,
2242 },
2243}
2244
2245#[derive(Debug, Clone)]
2246pub(crate) struct CircleTransitionDraftPolicy {
2247 pub roster: CircleRosterDraftPolicy,
2248 pub metadata_successor: bool,
2249}
2250
2251#[derive(Debug, Clone)]
2252pub(crate) struct CircleTransitionDraft {
2253 pub circle_id: CircleId,
2254 pub epoch_id: CircleEpochId,
2255 pub keyring: String,
2256 pub roster: CircleMaterializedRoster,
2257 pub policy: CircleTransitionDraftPolicy,
2258 pub metadata: CircleMetadata,
2259 pub close_intent: Option<CircleEpochCloseIntent>,
2260 pub close_finalization: Option<CircleEpochCloseFinalizationDraft>,
2261 pub close_cancellation: Option<CircleEpochCloseCancellationDraft>,
2262 pub access: Vec<PreparedCircleAccess>,
2263 pub control: PreparedCircleControl,
2264}
2265
2266#[derive(Debug, Clone)]
2267pub(crate) struct CircleEpochCloseFinalizationDraft {
2268 pub close_control: PreparedCircleControl,
2269 pub intent: CircleEpochCloseIntent,
2270 pub responses: Vec<CircleEpochCloseSettlement>,
2271 pub outcome_slot: ObjectSlot,
2272}
2273
2274#[derive(Debug, Clone)]
2275pub(crate) struct CircleEpochCloseCancellationDraft {
2276 pub close_control: PreparedCircleControl,
2277 pub outcome_slot: ObjectSlot,
2278}
2279
2280#[derive(Debug, Clone)]
2281struct FounderRosterObjects {
2282 entry: CircleRosterEntry,
2283 resolved: ResolvedCircleRoster,
2284}
2285
2286struct PreparedAccessMaterial {
2287 value: CircleAccessLeaf,
2288 bytes: Vec<u8>,
2289 leaf_hash: ObjectHash,
2290}
2291
2292#[allow(clippy::too_many_arguments)]
2293fn prepare_access_material(
2294 store_root_hash: ObjectHash,
2295 candidate_family: super::store_commit::CandidateFamilyId,
2296 circle_id: CircleId,
2297 epoch_id: CircleEpochId,
2298 keyring: &str,
2299 key_fingerprint: KeyFingerprint,
2300 roster_state: &CircleRosterStateRef,
2301 roster_members: &std::collections::BTreeMap<String, super::circle::CircleRole>,
2302 store_membership: &StoreMembershipStateRef,
2303 store_members: &[(String, MemberRole)],
2304 bootstraps: &std::collections::BTreeMap<String, CircleBootstrapRef>,
2305 ids: &dyn crate::id_provider::IdProvider,
2306 signer: &UserKeypair,
2307) -> Result<Vec<PreparedAccessMaterial>, CircleTransitionError> {
2308 let author_pubkey = keys::public_key_hex(signer);
2309 store_members
2310 .iter()
2311 .map(|(recipient_pubkey, _)| {
2312 let recipient_slot = recipient_slot(signer, recipient_pubkey, circle_id)?;
2313 let disposition = if roster_members.contains_key(recipient_pubkey) {
2314 CircleAccessDisposition::Active {
2315 keyring: keyring.to_string(),
2316 key_fingerprint,
2317 roster: roster_state.clone(),
2318 bootstrap: bootstraps.get(recipient_pubkey).cloned(),
2319 }
2320 } else {
2321 CircleAccessDisposition::Inactive
2322 };
2323 let mut value = CircleAccessLeaf {
2324 version: STORE_PROTOCOL_VERSION,
2325 store_root_hash,
2326 candidate_family,
2327 circle_id,
2328 epoch_id,
2329 leaf_id: AccessLeafId::generate(ids),
2330 owner_pubkey: author_pubkey.clone(),
2331 recipient_pubkey: recipient_pubkey.clone(),
2332 recipient_slot,
2333 disposition,
2334 store_membership: store_membership.clone(),
2335 signature: String::new(),
2336 };
2337 value.signature = keys::sign_hex(signer, &value.canonical_bytes()).1;
2338 let recipient_ed25519: [u8; keys::SIGN_PUBLICKEYBYTES] = hex::decode(recipient_pubkey)
2339 .map_err(|_| CircleTransitionError::InvalidRecipient(recipient_pubkey.clone()))?
2340 .try_into()
2341 .map_err(|_| CircleTransitionError::InvalidRecipient(recipient_pubkey.clone()))?;
2342 let recipient_x25519 = keys::ed25519_to_x25519_public_key(&recipient_ed25519)
2343 .map_err(|_| CircleTransitionError::InvalidRecipient(recipient_pubkey.clone()))?;
2344 let plaintext =
2345 serde_json::to_vec(&value).expect("circle access serialization cannot fail");
2346 let bytes = keys::seal_box_encrypt(&plaintext, &recipient_x25519);
2347 let leaf_hash = ObjectHash::digest(&bytes);
2348 Ok(PreparedAccessMaterial {
2349 value,
2350 bytes,
2351 leaf_hash,
2352 })
2353 })
2354 .collect()
2355}
2356
2357fn prepare_access_envelopes(
2358 store_root_hash: ObjectHash,
2359 candidate_family: super::store_commit::CandidateFamilyId,
2360 circle_id: CircleId,
2361 control: &PreparedCircleControl,
2362 leaves: Vec<PreparedAccessMaterial>,
2363 proofs: Vec<Vec<MerkleStep>>,
2364 signer: &UserKeypair,
2365) -> Vec<PreparedCircleAccess> {
2366 let author_pubkey = keys::public_key_hex(signer);
2367 leaves
2368 .into_iter()
2369 .zip(proofs)
2370 .map(|(leaf, proof)| {
2371 let mut envelope = AccessEnvelope {
2372 version: STORE_PROTOCOL_VERSION,
2373 store_root_hash,
2374 candidate_family,
2375 circle_id,
2376 owner_pubkey: author_pubkey.clone(),
2377 recipient_slot: leaf.value.recipient_slot.clone(),
2378 control_hash: control.coord.control_hash(),
2379 leaf_id: leaf.value.leaf_id,
2380 leaf_hash: leaf.leaf_hash,
2381 value_hash: ObjectHash::digest(
2382 &serde_json::to_vec(&leaf.value)
2383 .expect("circle access leaf serialization cannot fail"),
2384 ),
2385 proof,
2386 signature: String::new(),
2387 };
2388 envelope.signature = keys::sign_hex(signer, &envelope.canonical_bytes()).1;
2389 PreparedCircleAccess {
2390 leaf: PreparedAccessLeaf {
2391 bytes: leaf.bytes,
2392 value: leaf.value,
2393 leaf_hash: leaf.leaf_hash,
2394 },
2395 envelope,
2396 }
2397 })
2398 .collect()
2399}
2400
2401#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2402#[serde(deny_unknown_fields)]
2403pub struct PreparedCircleTransition {
2404 pub circle_id: CircleId,
2405 pub epoch_id: CircleEpochId,
2406 pub keyring: String,
2407 pub roster: CircleMaterializedRoster,
2408 pub policy_objects: CircleTransitionPolicyObjects,
2409 pub metadata: CircleMetadata,
2410 pub close_intent: Option<CircleEpochCloseIntent>,
2411 pub close_outcome: Option<CircleEpochCloseOutcome>,
2412 pub close_cancellation: Option<CircleEpochCloseCancellation>,
2413 pub access: Vec<PreparedCircleAccess>,
2414 pub control: PreparedCircleControl,
2415}
2416
2417impl PreparedCircleTransition {
2418 pub fn resolved_roster(&self) -> CircleMaterializedRoster {
2419 self.roster.clone()
2420 }
2421
2422 pub fn control_ref(
2423 &self,
2424 objects: super::store_commit::CircleActivationObjects,
2425 head_object: Option<ExactObjectRef>,
2426 ) -> super::store_commit::CircleControlRef {
2427 let head_object =
2428 head_object.expect("prepared Circle transition must contain its stored head");
2429 super::store_commit::CircleControlRef {
2430 circle_id: self.circle_id,
2431 control: self.control.coord.clone(),
2432 head_hash: self.policy_objects.control_head.head_hash(),
2433 head_object,
2434 objects,
2435 }
2436 }
2437}
2438
2439struct CircleSuccessorContext<'a> {
2440 store_members: Vec<(String, MemberRole)>,
2441 author_pubkey: String,
2442 epoch: &'a MergeActiveCircleEpoch,
2443 grant_id: MembershipGrantId,
2444 author_authority: MergeCircleOwnerAuthorityRef,
2445 key_fingerprint: KeyFingerprint,
2446}
2447
2448fn circle_successor_context<'a>(
2452 store_members: Vec<(String, MemberRole)>,
2453 current_control: &'a PreparedCircleControl,
2454 current_roster: &CircleMaterializedRoster,
2455 current_metadata: &CircleMetadata,
2456 keyring: &str,
2457 signer: &UserKeypair,
2458) -> Result<CircleSuccessorContext<'a>, CircleTransitionError> {
2459 let epoch = current_control
2460 .value
2461 .active_epoch()
2462 .ok_or(CircleTransitionError::InvalidCurrentState)?;
2463 circle_authored_successor_context(
2464 store_members,
2465 current_control,
2466 current_roster,
2467 current_metadata,
2468 keyring,
2469 signer,
2470 epoch,
2471 )
2472}
2473
2474fn circle_delete_successor_context<'a>(
2480 store_members: Vec<(String, MemberRole)>,
2481 current_control: &'a PreparedCircleControl,
2482 current_roster: &CircleMaterializedRoster,
2483 current_metadata: &CircleMetadata,
2484 keyring: &str,
2485 signer: &UserKeypair,
2486) -> Result<CircleSuccessorContext<'a>, CircleTransitionError> {
2487 let epoch = current_control.value.access_epoch();
2488 circle_authored_successor_context(
2489 store_members,
2490 current_control,
2491 current_roster,
2492 current_metadata,
2493 keyring,
2494 signer,
2495 epoch,
2496 )
2497}
2498
2499fn circle_authored_successor_context<'a>(
2500 mut store_members: Vec<(String, MemberRole)>,
2501 current_control: &PreparedCircleControl,
2502 current_roster: &CircleMaterializedRoster,
2503 current_metadata: &CircleMetadata,
2504 keyring: &str,
2505 signer: &UserKeypair,
2506 epoch: &'a MergeActiveCircleEpoch,
2507) -> Result<CircleSuccessorContext<'a>, CircleTransitionError> {
2508 if !current_control.verify()
2509 || !current_roster.verify()
2510 || !current_metadata.verify()
2511 || current_control.value.circle_id != current_metadata.circle_id
2512 || current_control.value.epoch_id() != current_metadata.epoch_id
2513 {
2514 return Err(CircleTransitionError::InvalidCurrentState);
2515 }
2516 let author_pubkey = keys::public_key_hex(signer);
2517 store_members.sort_by(|left, right| left.0.cmp(&right.0));
2518 store_members.dedup_by(|left, right| left.0 == right.0);
2519 if !store_members
2520 .iter()
2521 .any(|(pubkey, role)| pubkey == &author_pubkey && role.can_write())
2522 {
2523 return Err(CircleTransitionError::AuthorNotStoreWriter);
2524 }
2525 if current_roster.members().get(&author_pubkey) != Some(&super::circle::CircleRole::Owner) {
2526 return Err(CircleTransitionError::AuthorNotCircleOwner);
2527 }
2528 let key_fingerprint = EncryptionService::from(
2529 MasterKeyring::from_serialized(keyring)
2530 .map_err(|_| CircleTransitionError::InvalidCurrentState)?,
2531 )
2532 .seal_key_fingerprint();
2533 if key_fingerprint != current_control.value.key_fingerprint()
2534 || current_metadata.key_fingerprint != key_fingerprint
2535 {
2536 return Err(CircleTransitionError::InvalidCurrentState);
2537 }
2538 let (grant_id, record) = current_roster
2539 .active_grants()
2540 .find(|(_, record)| {
2541 record.member_pubkey == author_pubkey && record.role == super::circle::CircleRole::Owner
2542 })
2543 .ok_or(CircleTransitionError::AuthorNotCircleOwner)?;
2544 let author_authority = match &record.creation_authority {
2545 CircleGrantCreationAuthority::Entry(created_at) => MergeCircleOwnerAuthorityRef::Roster {
2546 roster: epoch.roster.clone(),
2547 grant_id: grant_id.clone(),
2548 created_at: created_at.clone(),
2549 },
2550 CircleGrantCreationAuthority::ConflictResolution(resolution) => {
2551 MergeCircleOwnerAuthorityRef::ConflictResolution {
2552 conflict_hash: resolution.conflict_hash,
2553 resolution_hash: resolution.resolution_hash,
2554 }
2555 }
2556 };
2557 Ok(CircleSuccessorContext {
2558 store_members,
2559 author_pubkey,
2560 epoch,
2561 grant_id: grant_id.clone(),
2562 author_authority,
2563 key_fingerprint,
2564 })
2565}
2566
2567impl CircleTransitionDraft {
2568 #[allow(clippy::too_many_arguments)]
2569 pub(crate) fn founder(
2570 store_root_hash: ObjectHash,
2571 candidate_family: super::store_commit::CandidateFamilyId,
2572 device_id: &str,
2573 name: &str,
2574 metadata_stamp: &str,
2575 store_membership: StoreMembershipStateRef,
2576 membership_authority: MembershipGrantCreationAuthority,
2577 mut store_members: Vec<(String, MemberRole)>,
2578 ids: &dyn crate::id_provider::IdProvider,
2579 signer: &UserKeypair,
2580 ) -> Result<Self, CircleTransitionError> {
2581 let author_pubkey = keys::public_key_hex(signer);
2582 store_members.sort_by(|left, right| left.0.cmp(&right.0));
2583 store_members.dedup_by(|left, right| left.0 == right.0);
2584 if !store_members
2585 .iter()
2586 .any(|(pubkey, role)| pubkey == &author_pubkey && role.can_write())
2587 {
2588 return Err(CircleTransitionError::AuthorNotStoreWriter);
2589 }
2590 let owner_grant =
2591 MembershipGrantId(generated_id_digest(ids, OWNER_GRANT_ID_GENERATION_DOMAIN));
2592 let author_stream_id = AuthorStreamId::from_digest(generated_id_digest(
2593 ids,
2594 b"coven.circle-transition-draft-stream.v1\0",
2595 ));
2596 let circle_id = CircleId::founder(store_root_hash, &author_pubkey, &owner_grant);
2597 let epoch_id = CircleEpochId::generate(ids);
2598 let keyring = MasterKeyring::generate();
2599 let encryption = EncryptionService::from(keyring.clone());
2600 let key_fingerprint = encryption.seal_key_fingerprint();
2601 let entry = CircleRosterEntry::founder(
2602 store_root_hash,
2603 circle_id,
2604 device_id,
2605 author_stream_id,
2606 owner_grant.clone(),
2607 signer,
2608 );
2609 let roster_objects = FounderRosterObjects {
2610 resolved: CircleRosterChain::from_entries(vec![entry.clone()])?.resolved(),
2611 entry,
2612 };
2613 let roster_state = MergeCircleRosterStateRef {
2614 heads: Vec::new(),
2615 resolutions: Vec::new(),
2616 state_hash: roster_objects.resolved.state_hash,
2617 };
2618 let metadata = CircleMetadata::founder(
2619 store_root_hash,
2620 circle_id,
2621 epoch_id,
2622 name,
2623 metadata_stamp,
2624 device_id,
2625 author_stream_id,
2626 owner_grant.clone(),
2627 roster_state.clone(),
2628 key_fingerprint,
2629 signer,
2630 )?;
2631 let metadata_state = MergeCircleMetadataStateRef {
2632 heads: Vec::new(),
2633 selected: metadata.coord(),
2634 state_hash: metadata.metadata_hash(),
2635 };
2636 let roster = roster_objects.resolved.clone();
2637 let leaves = prepare_access_material(
2638 store_root_hash,
2639 candidate_family,
2640 circle_id,
2641 epoch_id,
2642 &keyring.to_serialized(),
2643 key_fingerprint,
2644 &roster_state,
2645 &roster.members(),
2646 &store_membership,
2647 &store_members,
2648 &std::collections::BTreeMap::new(),
2649 ids,
2650 signer,
2651 )?;
2652 let leaf_hashes = leaves.iter().map(|leaf| leaf.leaf_hash).collect::<Vec<_>>();
2653 let (access_root, proofs) = merkle_root_and_proofs(&leaf_hashes);
2654 let common = ActiveCircleEpochCore {
2655 epoch_id,
2656 key_fingerprint,
2657 owners: vec![author_pubkey.clone()],
2658 access_root,
2659 origin: CircleEpochOrigin::Founder,
2660 };
2661 let value = CircleControlValue {
2662 order: MergeCircleControlOrder {
2663 device_id: device_id.to_string(),
2664 stream_id: author_stream_id,
2665 author_owner_grant: owner_grant.clone(),
2666 seq: 1,
2667 previous_control_hash: None,
2668 dependencies: Vec::new(),
2669 },
2670 state: CircleControlState::ActiveEpoch(MergeActiveCircleEpoch {
2671 common,
2672 metadata: metadata_state,
2673 roster: roster_state.clone(),
2674 store_membership,
2675 covered_control_heads: Vec::new(),
2676 }),
2677 author_authority: MergeCircleOwnerAuthorityRef::Roster {
2678 roster: roster_state,
2679 grant_id: owner_grant.clone(),
2680 created_at: roster_objects.entry.coord(),
2681 },
2682 membership_authority,
2683 };
2684 let mut control_value = CircleControl {
2685 version: STORE_PROTOCOL_VERSION,
2686 store_root_hash,
2687 circle_id,
2688 value,
2689 author_pubkey: author_pubkey.clone(),
2690 signature: String::new(),
2691 };
2692 control_value.signature = keys::sign_hex(signer, &control_value.canonical_bytes()).1;
2693 let control = PreparedCircleControl {
2694 coord: control_value.coord(),
2695 bytes: serde_json::to_vec(&control_value)
2696 .expect("circle control serialization cannot fail"),
2697 value: control_value,
2698 };
2699 let policy_objects = CircleTransitionDraftPolicy {
2700 roster: CircleRosterDraftPolicy::Founder {
2701 entry: roster_objects.entry,
2702 },
2703 metadata_successor: true,
2704 };
2705 let access = prepare_access_envelopes(
2706 store_root_hash,
2707 candidate_family,
2708 circle_id,
2709 &control,
2710 leaves,
2711 proofs,
2712 signer,
2713 );
2714 Ok(Self {
2715 circle_id,
2716 epoch_id,
2717 keyring: keyring.to_serialized(),
2718 roster,
2719 policy: policy_objects,
2720 metadata,
2721 close_intent: None,
2722 close_finalization: None,
2723 close_cancellation: None,
2724 access,
2725 control,
2726 })
2727 }
2728
2729 #[allow(clippy::too_many_arguments)]
2730 pub(crate) fn add_member(
2731 candidate_family: super::store_commit::CandidateFamilyId,
2732 device_id: &str,
2733 store_membership: StoreMembershipStateRef,
2734 membership_authority: MembershipGrantCreationAuthority,
2735 store_members: Vec<(String, MemberRole)>,
2736 current_control: &PreparedCircleControl,
2737 current_roster: &CircleMaterializedRoster,
2738 current_roster_chain: CircleRosterChain,
2739 current_metadata: &CircleMetadata,
2740 keyring: &str,
2741 roster_stream: AuthorStreamId,
2742 member_pubkey: String,
2743 role: super::circle::CircleRole,
2744 bootstrap: CircleBootstrapRef,
2745 ids: &dyn crate::id_provider::IdProvider,
2746 signer: &UserKeypair,
2747 ) -> Result<Self, CircleTransitionError> {
2748 if current_roster_chain.try_resolved()? != *current_roster {
2749 return Err(CircleTransitionError::InvalidCurrentState);
2750 }
2751 let context = circle_successor_context(
2752 store_members,
2753 current_control,
2754 current_roster,
2755 current_metadata,
2756 keyring,
2757 signer,
2758 )?;
2759 let CircleSuccessorContext {
2760 store_members,
2761 author_pubkey,
2762 epoch: active_epoch,
2763 grant_id,
2764 author_authority,
2765 key_fingerprint,
2766 } = context;
2767 if !store_members
2768 .iter()
2769 .any(|(pubkey, _)| pubkey == &member_pubkey)
2770 {
2771 return Err(CircleTransitionError::MemberNotInStore(member_pubkey));
2772 }
2773 let entry = current_roster_chain.signed_set_member(
2774 device_id,
2775 roster_stream,
2776 member_pubkey.clone(),
2777 role,
2778 signer,
2779 )?;
2780 let roster = current_roster_chain.resolved_with_successor(entry.clone())?;
2781 let roster_state = MergeCircleRosterStateRef {
2782 heads: active_epoch.roster.heads.clone(),
2783 resolutions: active_epoch.roster.resolutions.clone(),
2784 state_hash: roster.state_hash,
2785 };
2786 let store_root_hash = current_control.value.store_root_hash;
2787 let circle_id = current_control.value.circle_id;
2788 let epoch_id = current_control.value.epoch_id();
2789 let mut control_value = CircleControl {
2790 version: STORE_PROTOCOL_VERSION,
2791 store_root_hash,
2792 circle_id,
2793 value: CircleControlValue {
2794 order: MergeCircleControlOrder {
2795 device_id: device_id.to_string(),
2796 stream_id: roster_stream,
2797 author_owner_grant: grant_id.clone(),
2798 seq: current_control
2799 .value
2800 .ordinal()
2801 .checked_add(1)
2802 .ok_or(CircleTransitionError::SequenceOverflow)?,
2803 previous_control_hash: Some(current_control.coord.control_hash()),
2804 dependencies: vec![current_control.coord.clone()],
2805 },
2806 state: CircleControlState::ActiveEpoch(MergeActiveCircleEpoch {
2807 common: active_epoch.common.clone(),
2808 metadata: active_epoch.metadata.clone(),
2809 roster: roster_state.clone(),
2810 store_membership,
2811 covered_control_heads: active_epoch.covered_control_heads.clone(),
2812 }),
2813 author_authority,
2814 membership_authority,
2815 },
2816 author_pubkey,
2817 signature: String::new(),
2818 };
2819 let bootstraps =
2820 std::collections::BTreeMap::from([(member_pubkey.clone(), bootstrap.clone())]);
2821 let leaves = prepare_access_material(
2822 store_root_hash,
2823 candidate_family,
2824 circle_id,
2825 epoch_id,
2826 keyring,
2827 key_fingerprint,
2828 &roster_state,
2829 &roster.members(),
2830 &control_value
2831 .value
2832 .state
2833 .active_epoch()
2834 .expect("member addition constructs an active epoch")
2835 .store_membership,
2836 &store_members,
2837 &bootstraps,
2838 ids,
2839 signer,
2840 )?;
2841 let leaf_hashes = leaves.iter().map(|leaf| leaf.leaf_hash).collect::<Vec<_>>();
2842 let (access_root, proofs) = merkle_root_and_proofs(&leaf_hashes);
2843 control_value
2844 .value
2845 .state
2846 .active_epoch_mut()
2847 .expect("member addition constructs an active epoch")
2848 .common
2849 .access_root = access_root;
2850 control_value.signature = keys::sign_hex(signer, &control_value.canonical_bytes()).1;
2851 let control = PreparedCircleControl {
2852 coord: control_value.coord(),
2853 bytes: serde_json::to_vec(&control_value)
2854 .expect("circle control serialization cannot fail"),
2855 value: control_value,
2856 };
2857 let access = prepare_access_envelopes(
2858 store_root_hash,
2859 candidate_family,
2860 circle_id,
2861 &control,
2862 leaves,
2863 proofs,
2864 signer,
2865 );
2866 Ok(Self {
2867 circle_id,
2868 epoch_id,
2869 keyring: keyring.to_string(),
2870 roster,
2871 policy: CircleTransitionDraftPolicy {
2872 roster: CircleRosterDraftPolicy::Successor {
2873 predecessor: current_roster_chain,
2874 entry,
2875 },
2876 metadata_successor: false,
2877 },
2878 metadata: current_metadata.clone(),
2879 close_intent: None,
2880 close_finalization: None,
2881 close_cancellation: None,
2882 access,
2883 control,
2884 })
2885 }
2886
2887 #[allow(clippy::too_many_arguments)]
2888 pub(crate) fn close_epoch(
2889 candidate_family: super::store_commit::CandidateFamilyId,
2890 device_id: &str,
2891 store_membership: StoreMembershipStateRef,
2892 membership_authority: MembershipGrantCreationAuthority,
2893 store_members: Vec<(String, MemberRole)>,
2894 current_control: &PreparedCircleControl,
2895 current_roster: &CircleMaterializedRoster,
2896 current_metadata: &CircleMetadata,
2897 keyring: &str,
2898 close_id: CircleEpochCloseId,
2899 close_intent: CircleEpochCloseIntent,
2900 intent: CircleEpochCloseIntentRef,
2901 frozen_device_state: StoreDeviceStateRef,
2902 participants: Vec<CircleEpochCloseParticipant>,
2903 provisional_frontier: CommitFrontier,
2904 outcome_slot: ObjectSlot,
2905 ids: &dyn crate::id_provider::IdProvider,
2906 signer: &UserKeypair,
2907 ) -> Result<Self, CircleTransitionError> {
2908 let context = circle_successor_context(
2909 store_members,
2910 current_control,
2911 current_roster,
2912 current_metadata,
2913 keyring,
2914 signer,
2915 )?;
2916 let CircleSuccessorContext {
2917 store_members,
2918 author_pubkey,
2919 epoch: active_epoch,
2920 grant_id,
2921 author_authority,
2922 key_fingerprint,
2923 } = context;
2924 let store_root_hash = current_control.value.store_root_hash;
2925 let circle_id = current_control.value.circle_id;
2926 let epoch_id = current_control.value.epoch_id();
2927 if close_intent.close_id != close_id
2928 || close_intent.intent_hash() != intent.intent_hash
2929 || close_intent.circle_id != circle_id
2930 || close_intent.epoch_id != epoch_id
2931 {
2932 return Err(CircleTransitionError::InvalidCurrentState);
2933 }
2934 let roster_state = active_epoch.roster.clone();
2935 let mut control_value = CircleControl {
2936 version: STORE_PROTOCOL_VERSION,
2937 store_root_hash,
2938 circle_id,
2939 value: CircleControlValue {
2940 order: MergeCircleControlOrder {
2941 device_id: device_id.to_string(),
2942 stream_id: active_epoch
2943 .covered_control_heads
2944 .iter()
2945 .find(|head| {
2946 head.coord.author_pubkey == author_pubkey
2947 && head.coord.device_id == device_id
2948 && head.coord.author_owner_grant == grant_id
2949 })
2950 .map_or_else(
2951 || {
2952 AuthorStreamId::from_digest(generated_id_digest(
2953 ids,
2954 b"coven.circle-transition-draft-stream.v1\0",
2955 ))
2956 },
2957 |head| head.coord.stream_id,
2958 ),
2959 author_owner_grant: grant_id,
2960 seq: current_control
2961 .value
2962 .ordinal()
2963 .checked_add(1)
2964 .ok_or(CircleTransitionError::SequenceOverflow)?,
2965 previous_control_hash: Some(current_control.coord.control_hash()),
2966 dependencies: vec![current_control.coord.clone()],
2967 },
2968 state: CircleControlState::EpochClose(CircleEpochClose {
2969 close_id,
2970 frozen_epoch: MergeActiveCircleEpoch {
2971 common: active_epoch.common.clone(),
2972 metadata: active_epoch.metadata.clone(),
2973 roster: roster_state.clone(),
2974 store_membership,
2975 covered_control_heads: active_epoch.covered_control_heads.clone(),
2976 },
2977 intent,
2978 frozen_device_state,
2979 participants,
2980 provisional_frontier,
2981 outcome_slot,
2982 }),
2983 author_authority,
2984 membership_authority,
2985 },
2986 author_pubkey,
2987 signature: String::new(),
2988 };
2989 let leaves = prepare_access_material(
2990 store_root_hash,
2991 candidate_family,
2992 circle_id,
2993 epoch_id,
2994 keyring,
2995 key_fingerprint,
2996 &roster_state,
2997 ¤t_roster.members(),
2998 &control_value.access_epoch().store_membership,
2999 &store_members,
3000 &std::collections::BTreeMap::new(),
3001 ids,
3002 signer,
3003 )?;
3004 let leaf_hashes = leaves.iter().map(|leaf| leaf.leaf_hash).collect::<Vec<_>>();
3005 let (access_root, proofs) = merkle_root_and_proofs(&leaf_hashes);
3006 control_value
3007 .value
3008 .state
3009 .access_epoch_mut()
3010 .common
3011 .access_root = access_root;
3012 control_value.signature = keys::sign_hex(signer, &control_value.canonical_bytes()).1;
3013 let control = PreparedCircleControl {
3014 coord: control_value.coord(),
3015 bytes: serde_json::to_vec(&control_value)
3016 .expect("circle control serialization cannot fail"),
3017 value: control_value,
3018 };
3019 let access = prepare_access_envelopes(
3020 store_root_hash,
3021 candidate_family,
3022 circle_id,
3023 &control,
3024 leaves,
3025 proofs,
3026 signer,
3027 );
3028 Ok(Self {
3029 circle_id,
3030 epoch_id,
3031 keyring: keyring.to_string(),
3032 roster: current_roster.clone(),
3033 policy: CircleTransitionDraftPolicy {
3034 roster: CircleRosterDraftPolicy::Inherited,
3035 metadata_successor: false,
3036 },
3037 metadata: current_metadata.clone(),
3038 close_intent: Some(close_intent),
3039 close_finalization: None,
3040 close_cancellation: None,
3041 access,
3042 control,
3043 })
3044 }
3045
3046 #[allow(clippy::too_many_arguments)]
3047 pub(crate) fn finalize_epoch_close(
3048 candidate_family: super::store_commit::CandidateFamilyId,
3049 device_id: &str,
3050 author_registration: &StoreDeviceRegistrationRef,
3051 metadata_stamp: &str,
3052 store_membership: StoreMembershipStateRef,
3053 membership_authority: MembershipGrantCreationAuthority,
3054 mut store_members: Vec<(String, MemberRole)>,
3055 close_control: &PreparedCircleControl,
3056 current_roster: &CircleMaterializedRoster,
3057 current_roster_chain: CircleRosterChain,
3058 current_metadata: &CircleMetadata,
3059 keyring: &str,
3060 intent: CircleEpochCloseIntent,
3061 responses: Vec<CircleEpochCloseSettlement>,
3062 ids: &dyn crate::id_provider::IdProvider,
3063 signer: &UserKeypair,
3064 ) -> Result<Self, CircleTransitionError> {
3065 let CircleControlState::EpochClose(close) = close_control.value.state() else {
3066 return Err(CircleTransitionError::InvalidCurrentState);
3067 };
3068 if !close_control.verify()
3069 || current_roster_chain.try_resolved()? != *current_roster
3070 || current_roster.state_hash() != close.frozen_epoch.roster.state_hash
3071 || current_metadata.coord() != close.frozen_epoch.metadata.selected
3072 || current_metadata.epoch_id != close.frozen_epoch.common.epoch_id
3073 || current_metadata.key_fingerprint != close.frozen_epoch.common.key_fingerprint
3074 || intent.intent_hash() != close.intent.intent_hash
3075 || intent.close_id != close.close_id
3076 {
3077 return Err(CircleTransitionError::InvalidCurrentState);
3078 }
3079 let author_pubkey = keys::public_key_hex(signer);
3080 store_members.sort_by(|left, right| left.0.cmp(&right.0));
3081 store_members.dedup_by(|left, right| left.0 == right.0);
3082 if !store_members
3083 .iter()
3084 .any(|(pubkey, role)| pubkey == &author_pubkey && role.can_write())
3085 {
3086 return Err(CircleTransitionError::AuthorNotStoreWriter);
3087 }
3088 let (grant_id, owner_record) = current_roster
3089 .active_grants()
3090 .find(|(_, record)| {
3091 record.member_pubkey == author_pubkey
3092 && record.role == super::circle::CircleRole::Owner
3093 })
3094 .ok_or(CircleTransitionError::AuthorNotCircleOwner)?;
3095 let author_authority = match &owner_record.creation_authority {
3096 CircleGrantCreationAuthority::Entry(created_at) => {
3097 MergeCircleOwnerAuthorityRef::Roster {
3098 roster: close.frozen_epoch.roster.clone(),
3099 grant_id: grant_id.clone(),
3100 created_at: created_at.clone(),
3101 }
3102 }
3103 CircleGrantCreationAuthority::ConflictResolution(resolution) => {
3104 MergeCircleOwnerAuthorityRef::ConflictResolution {
3105 conflict_hash: resolution.conflict_hash,
3106 resolution_hash: resolution.resolution_hash,
3107 }
3108 }
3109 };
3110 let old_encryption = EncryptionService::from(
3111 MasterKeyring::from_serialized(keyring)
3112 .map_err(|_| CircleTransitionError::InvalidCurrentState)?,
3113 );
3114 if old_encryption.seal_key_fingerprint() != close.frozen_epoch.common.key_fingerprint {
3115 return Err(CircleTransitionError::InvalidCurrentState);
3116 }
3117 let new_generation = old_encryption
3118 .current_generation()
3119 .checked_add(1)
3120 .ok_or(CircleTransitionError::SequenceOverflow)?;
3121 let encryption = old_encryption
3122 .with_appended_generation(new_generation, crate::encryption::generate_random_key())
3123 .map_err(|_| CircleTransitionError::InvalidCurrentState)?;
3124 let keyring = encryption
3125 .to_keyring_string()
3126 .map_err(|_| CircleTransitionError::InvalidCurrentState)?;
3127 let key_fingerprint = encryption.seal_key_fingerprint();
3128 let epoch_id = CircleEpochId::generate(ids);
3129 let roster = current_roster_chain.resolved_with_successor(intent.removal.clone())?;
3130 if roster.state_hash() != intent.remaining_roster_state_hash {
3131 return Err(CircleTransitionError::InvalidCurrentState);
3132 }
3133 let roster_members = roster.members();
3134 let owners = roster_members
3135 .iter()
3136 .filter_map(|(pubkey, role)| {
3137 (*role == super::circle::CircleRole::Owner).then_some(pubkey.clone())
3138 })
3139 .collect::<Vec<_>>();
3140 if owners.is_empty() {
3141 return Err(CircleTransitionError::InvalidCurrentState);
3142 }
3143 let roster_state = MergeCircleRosterStateRef {
3144 heads: close.frozen_epoch.roster.heads.clone(),
3145 resolutions: close.frozen_epoch.roster.resolutions.clone(),
3146 state_hash: roster.state_hash(),
3147 };
3148 let metadata_stream = super::store_commit::StreamActivation::grant_authorized_stream_id(
3149 close_control.value.store_root_hash,
3150 author_registration,
3151 grant_id,
3152 super::store_commit::StreamAnchorDomain::CircleMetadata {
3153 circle_id: close_control.value.circle_id,
3154 },
3155 );
3156 let prior_metadata = close
3157 .frozen_epoch
3158 .metadata
3159 .heads
3160 .iter()
3161 .find(|head| head.coord.stream_id == metadata_stream);
3162 let mut metadata = current_metadata.clone();
3163 metadata.epoch_id = epoch_id;
3164 metadata.metadata_stamp = metadata_stamp.to_string();
3165 metadata.author_pubkey = author_pubkey.clone();
3166 metadata.device_id = device_id.to_string();
3167 metadata.stream_id = metadata_stream;
3168 metadata.author_owner_grant = grant_id.clone();
3169 metadata.seq = prior_metadata.map_or(Ok(1), |head| {
3170 head.coord
3171 .seq
3172 .checked_add(1)
3173 .ok_or(CircleTransitionError::SequenceOverflow)
3174 })?;
3175 metadata.previous_hash = prior_metadata.map(|head| head.coord.metadata_hash);
3176 metadata.dependencies = close
3177 .frozen_epoch
3178 .metadata
3179 .heads
3180 .iter()
3181 .map(|head| head.coord.clone())
3182 .collect();
3183 metadata.author_roster = roster_state.clone();
3184 metadata.key_fingerprint = key_fingerprint;
3185 metadata.signature = keys::sign_hex(signer, &metadata.canonical_bytes()).1;
3186 let metadata_state = MergeCircleMetadataStateRef {
3187 heads: close.frozen_epoch.metadata.heads.clone(),
3188 selected: metadata.coord(),
3189 state_hash: metadata.metadata_hash(),
3190 };
3191 let mut control_value = CircleControl {
3192 version: STORE_PROTOCOL_VERSION,
3193 store_root_hash: close_control.value.store_root_hash,
3194 circle_id: close_control.value.circle_id,
3195 value: CircleControlValue {
3196 order: MergeCircleControlOrder {
3197 device_id: device_id.to_string(),
3198 stream_id: close_control.value.value.order.stream_id,
3199 author_owner_grant: grant_id.clone(),
3200 seq: close_control
3201 .value
3202 .ordinal()
3203 .checked_add(1)
3204 .ok_or(CircleTransitionError::SequenceOverflow)?,
3205 previous_control_hash: Some(close_control.coord.control_hash()),
3206 dependencies: vec![close_control.coord.clone()],
3207 },
3208 state: CircleControlState::ActiveEpoch(MergeActiveCircleEpoch {
3209 common: ActiveCircleEpochCore {
3210 epoch_id,
3211 key_fingerprint,
3212 owners,
3213 access_root: close.frozen_epoch.common.access_root,
3214 origin: close.frozen_epoch.common.origin.clone(),
3215 },
3216 metadata: metadata_state,
3217 roster: roster_state.clone(),
3218 store_membership,
3219 covered_control_heads: close.frozen_epoch.covered_control_heads.clone(),
3220 }),
3221 author_authority,
3222 membership_authority,
3223 },
3224 author_pubkey,
3225 signature: String::new(),
3226 };
3227 let leaves = prepare_access_material(
3228 control_value.store_root_hash,
3229 candidate_family,
3230 control_value.circle_id,
3231 epoch_id,
3232 &keyring,
3233 key_fingerprint,
3234 &roster_state,
3235 &roster_members,
3236 &control_value.access_epoch().store_membership,
3237 &store_members,
3238 &std::collections::BTreeMap::new(),
3239 ids,
3240 signer,
3241 )?;
3242 let leaf_hashes = leaves.iter().map(|leaf| leaf.leaf_hash).collect::<Vec<_>>();
3243 let (access_root, proofs) = merkle_root_and_proofs(&leaf_hashes);
3244 control_value
3245 .value
3246 .state
3247 .active_epoch_mut()
3248 .expect("Circle finalization constructs an active epoch")
3249 .common
3250 .access_root = access_root;
3251 let control = PreparedCircleControl {
3252 coord: control_value.coord(),
3253 bytes: serde_json::to_vec(&control_value)
3254 .expect("Circle control serialization cannot fail"),
3255 value: control_value,
3256 };
3257 let access = prepare_access_envelopes(
3258 control.value.store_root_hash,
3259 candidate_family,
3260 control.value.circle_id,
3261 &control,
3262 leaves,
3263 proofs,
3264 signer,
3265 );
3266 Ok(Self {
3267 circle_id: control.value.circle_id,
3268 epoch_id,
3269 keyring,
3270 roster,
3271 policy: CircleTransitionDraftPolicy {
3272 roster: CircleRosterDraftPolicy::Successor {
3273 predecessor: current_roster_chain,
3274 entry: intent.removal.clone(),
3275 },
3276 metadata_successor: true,
3277 },
3278 metadata,
3279 close_intent: None,
3280 close_finalization: Some(CircleEpochCloseFinalizationDraft {
3281 close_control: close_control.clone(),
3282 intent,
3283 responses,
3284 outcome_slot: close.outcome_slot.clone(),
3285 }),
3286 close_cancellation: None,
3287 access,
3288 control,
3289 })
3290 }
3291
3292 #[allow(clippy::too_many_arguments)]
3297 pub(crate) fn reopen_epoch(
3298 candidate_family: super::store_commit::CandidateFamilyId,
3299 device_id: &str,
3300 store_membership: StoreMembershipStateRef,
3301 membership_authority: MembershipGrantCreationAuthority,
3302 mut store_members: Vec<(String, MemberRole)>,
3303 close_control: &PreparedCircleControl,
3304 current_roster: &CircleMaterializedRoster,
3305 current_metadata: &CircleMetadata,
3306 keyring: &str,
3307 ids: &dyn crate::id_provider::IdProvider,
3308 signer: &UserKeypair,
3309 ) -> Result<Self, CircleTransitionError> {
3310 let CircleControlState::EpochClose(close) = close_control.value.state() else {
3311 return Err(CircleTransitionError::InvalidCurrentState);
3312 };
3313 let frozen = &close.frozen_epoch;
3314 if !close_control.verify()
3315 || current_roster.state_hash() != frozen.roster.state_hash
3316 || current_metadata.coord() != frozen.metadata.selected
3317 || current_metadata.epoch_id != frozen.common.epoch_id
3318 || current_metadata.key_fingerprint != frozen.common.key_fingerprint
3319 {
3320 return Err(CircleTransitionError::InvalidCurrentState);
3321 }
3322 let author_pubkey = keys::public_key_hex(signer);
3323 store_members.sort_by(|left, right| left.0.cmp(&right.0));
3324 store_members.dedup_by(|left, right| left.0 == right.0);
3325 if !store_members
3326 .iter()
3327 .any(|(pubkey, role)| pubkey == &author_pubkey && role.can_write())
3328 {
3329 return Err(CircleTransitionError::AuthorNotStoreWriter);
3330 }
3331 let (grant_id, owner_record) = current_roster
3332 .active_grants()
3333 .find(|(_, record)| {
3334 record.member_pubkey == author_pubkey
3335 && record.role == super::circle::CircleRole::Owner
3336 })
3337 .ok_or(CircleTransitionError::AuthorNotCircleOwner)?;
3338 let author_authority = match &owner_record.creation_authority {
3339 CircleGrantCreationAuthority::Entry(created_at) => {
3340 MergeCircleOwnerAuthorityRef::Roster {
3341 roster: frozen.roster.clone(),
3342 grant_id: grant_id.clone(),
3343 created_at: created_at.clone(),
3344 }
3345 }
3346 CircleGrantCreationAuthority::ConflictResolution(resolution) => {
3347 MergeCircleOwnerAuthorityRef::ConflictResolution {
3348 conflict_hash: resolution.conflict_hash,
3349 resolution_hash: resolution.resolution_hash,
3350 }
3351 }
3352 };
3353 let encryption = EncryptionService::from(
3354 MasterKeyring::from_serialized(keyring)
3355 .map_err(|_| CircleTransitionError::InvalidCurrentState)?,
3356 );
3357 let key_fingerprint = encryption.seal_key_fingerprint();
3358 if key_fingerprint != frozen.common.key_fingerprint {
3359 return Err(CircleTransitionError::InvalidCurrentState);
3360 }
3361 let epoch_id = frozen.common.epoch_id;
3362 let roster_state = frozen.roster.clone();
3363 let mut control_value = CircleControl {
3364 version: STORE_PROTOCOL_VERSION,
3365 store_root_hash: close_control.value.store_root_hash,
3366 circle_id: close_control.value.circle_id,
3367 value: CircleControlValue {
3368 order: MergeCircleControlOrder {
3369 device_id: device_id.to_string(),
3370 stream_id: close_control.value.value.order.stream_id,
3371 author_owner_grant: grant_id.clone(),
3372 seq: close_control
3373 .value
3374 .ordinal()
3375 .checked_add(1)
3376 .ok_or(CircleTransitionError::SequenceOverflow)?,
3377 previous_control_hash: Some(close_control.coord.control_hash()),
3378 dependencies: vec![close_control.coord.clone()],
3379 },
3380 state: CircleControlState::ActiveEpoch(MergeActiveCircleEpoch {
3381 common: ActiveCircleEpochCore {
3382 epoch_id,
3383 key_fingerprint,
3384 owners: frozen.common.owners.clone(),
3385 access_root: frozen.common.access_root,
3386 origin: frozen.common.origin.clone(),
3387 },
3388 metadata: frozen.metadata.clone(),
3389 roster: roster_state.clone(),
3390 store_membership,
3391 covered_control_heads: frozen.covered_control_heads.clone(),
3392 }),
3393 author_authority,
3394 membership_authority,
3395 },
3396 author_pubkey,
3397 signature: String::new(),
3398 };
3399 let leaves = prepare_access_material(
3400 control_value.store_root_hash,
3401 candidate_family,
3402 control_value.circle_id,
3403 epoch_id,
3404 keyring,
3405 key_fingerprint,
3406 &roster_state,
3407 ¤t_roster.members(),
3408 &control_value.access_epoch().store_membership,
3409 &store_members,
3410 &std::collections::BTreeMap::new(),
3411 ids,
3412 signer,
3413 )?;
3414 let leaf_hashes = leaves.iter().map(|leaf| leaf.leaf_hash).collect::<Vec<_>>();
3415 let (access_root, proofs) = merkle_root_and_proofs(&leaf_hashes);
3416 control_value
3417 .value
3418 .state
3419 .active_epoch_mut()
3420 .expect("Circle reopen constructs an active epoch")
3421 .common
3422 .access_root = access_root;
3423 control_value.signature = keys::sign_hex(signer, &control_value.canonical_bytes()).1;
3424 let control = PreparedCircleControl {
3425 coord: control_value.coord(),
3426 bytes: serde_json::to_vec(&control_value)
3427 .expect("Circle control serialization cannot fail"),
3428 value: control_value,
3429 };
3430 let access = prepare_access_envelopes(
3431 control.value.store_root_hash,
3432 candidate_family,
3433 control.value.circle_id,
3434 &control,
3435 leaves,
3436 proofs,
3437 signer,
3438 );
3439 Ok(Self {
3440 circle_id: control.value.circle_id,
3441 epoch_id,
3442 keyring: keyring.to_string(),
3443 roster: current_roster.clone(),
3444 policy: CircleTransitionDraftPolicy {
3445 roster: CircleRosterDraftPolicy::Inherited,
3446 metadata_successor: false,
3447 },
3448 metadata: current_metadata.clone(),
3449 close_intent: None,
3450 close_finalization: None,
3451 close_cancellation: Some(CircleEpochCloseCancellationDraft {
3452 close_control: close_control.clone(),
3453 outcome_slot: close.outcome_slot.clone(),
3454 }),
3455 access,
3456 control,
3457 })
3458 }
3459
3460 #[allow(clippy::too_many_arguments)]
3461 pub(crate) fn rename(
3462 candidate_family: super::store_commit::CandidateFamilyId,
3463 device_id: &str,
3464 name: &str,
3465 metadata_stamp: &str,
3466 store_membership: StoreMembershipStateRef,
3467 membership_authority: MembershipGrantCreationAuthority,
3468 store_members: Vec<(String, MemberRole)>,
3469 current_control: &PreparedCircleControl,
3470 current_roster: &CircleMaterializedRoster,
3471 current_metadata: &CircleMetadata,
3472 keyring: &str,
3473 ids: &dyn crate::id_provider::IdProvider,
3474 signer: &UserKeypair,
3475 ) -> Result<Self, CircleTransitionError> {
3476 if name.trim().is_empty() {
3477 return Err(CircleTransitionError::EmptyName);
3478 }
3479 let context = circle_successor_context(
3480 store_members,
3481 current_control,
3482 current_roster,
3483 current_metadata,
3484 keyring,
3485 signer,
3486 )?;
3487 let CircleSuccessorContext {
3488 store_members,
3489 author_pubkey,
3490 epoch: active_epoch,
3491 grant_id,
3492 author_authority,
3493 key_fingerprint,
3494 } = context;
3495 let store_root_hash = current_control.value.store_root_hash;
3496 let circle_id = current_control.value.circle_id;
3497 let epoch_id = current_control.value.epoch_id();
3498 let roster_state = current_control.value.roster_state_ref();
3499
3500 let own_head = active_epoch.metadata.heads.iter().find(|head| {
3501 head.coord.author_pubkey == author_pubkey
3502 && head.coord.device_id == device_id
3503 && head.coord.author_owner_grant == grant_id
3504 });
3505 let author_stream_id = own_head.map_or_else(
3506 || {
3507 AuthorStreamId::from_digest(generated_id_digest(
3508 ids,
3509 b"coven.circle-transition-draft-stream.v1\0",
3510 ))
3511 },
3512 |head| head.coord.stream_id,
3513 );
3514 let metadata_seq = match own_head {
3515 Some(head) => head
3516 .coord
3517 .seq
3518 .checked_add(1)
3519 .ok_or(CircleTransitionError::SequenceOverflow)?,
3520 None => 1,
3521 };
3522 let metadata_previous = own_head.map(|head| head.coord.metadata_hash);
3523 let metadata_dependencies = active_epoch
3524 .metadata
3525 .heads
3526 .iter()
3527 .map(|head| head.coord.clone())
3528 .collect::<Vec<_>>();
3529 let metadata_state = active_epoch.metadata.clone();
3530 let mut control_value = CircleControlValue {
3531 order: MergeCircleControlOrder {
3532 device_id: device_id.to_string(),
3533 stream_id: author_stream_id,
3534 author_owner_grant: grant_id.clone(),
3535 seq: current_control
3536 .value
3537 .ordinal()
3538 .checked_add(1)
3539 .ok_or(CircleTransitionError::SequenceOverflow)?,
3540 previous_control_hash: Some(current_control.coord.control_hash()),
3541 dependencies: vec![current_control.coord.clone()],
3542 },
3543 state: CircleControlState::ActiveEpoch(MergeActiveCircleEpoch {
3544 common: active_epoch.common.clone(),
3545 metadata: active_epoch.metadata.clone(),
3546 roster: active_epoch.roster.clone(),
3547 store_membership,
3548 covered_control_heads: active_epoch.covered_control_heads.clone(),
3549 }),
3550 author_authority,
3551 membership_authority,
3552 };
3553 let author_owner_grant = grant_id.clone();
3554
3555 let mut metadata = CircleMetadata {
3556 version: STORE_PROTOCOL_VERSION,
3557 store_root_hash,
3558 circle_id,
3559 epoch_id,
3560 name: name.to_string(),
3561 seq: metadata_seq,
3562 previous_hash: metadata_previous,
3563 dependencies: metadata_dependencies,
3564 metadata_stamp: metadata_stamp.to_string(),
3565 author_pubkey: author_pubkey.clone(),
3566 device_id: device_id.to_string(),
3567 stream_id: author_stream_id,
3568 author_owner_grant,
3569 author_roster: roster_state.clone(),
3570 key_fingerprint,
3571 signature: String::new(),
3572 };
3573 metadata.signature = keys::sign_hex(signer, &metadata.canonical_bytes()).1;
3574
3575 let mut metadata_state = metadata_state;
3576 let selected = [current_metadata, &metadata]
3577 .into_iter()
3578 .max_by_key(|entry| {
3579 (
3580 entry.metadata_stamp.as_str(),
3581 entry.author_pubkey.as_str(),
3582 entry.device_id.as_str(),
3583 entry.metadata_hash(),
3584 )
3585 })
3586 .expect("current and successor metadata are non-empty");
3587 metadata_state.selected = selected.coord();
3588 metadata_state.state_hash = selected.metadata_hash();
3589
3590 let leaves = prepare_access_material(
3591 store_root_hash,
3592 candidate_family,
3593 circle_id,
3594 epoch_id,
3595 keyring,
3596 key_fingerprint,
3597 &roster_state,
3598 ¤t_roster.members(),
3599 &control_value
3600 .state
3601 .active_epoch()
3602 .expect("rename constructs an active epoch")
3603 .store_membership,
3604 &store_members,
3605 &std::collections::BTreeMap::new(),
3606 ids,
3607 signer,
3608 )?;
3609 let leaf_hashes = leaves.iter().map(|leaf| leaf.leaf_hash).collect::<Vec<_>>();
3610 let (access_root, proofs) = merkle_root_and_proofs(&leaf_hashes);
3611 let active_epoch = control_value
3612 .state
3613 .active_epoch_mut()
3614 .expect("rename constructs an active epoch");
3615 active_epoch.common.access_root = access_root;
3616 active_epoch.metadata = metadata_state;
3617 let mut control_value = CircleControl {
3618 version: STORE_PROTOCOL_VERSION,
3619 store_root_hash,
3620 circle_id,
3621 value: control_value,
3622 author_pubkey,
3623 signature: String::new(),
3624 };
3625 control_value.signature = keys::sign_hex(signer, &control_value.canonical_bytes()).1;
3626 let control = PreparedCircleControl {
3627 coord: control_value.coord(),
3628 bytes: serde_json::to_vec(&control_value)
3629 .expect("circle control serialization cannot fail"),
3630 value: control_value,
3631 };
3632 let policy_objects = CircleTransitionDraftPolicy {
3633 roster: CircleRosterDraftPolicy::Inherited,
3634 metadata_successor: true,
3635 };
3636 let access = prepare_access_envelopes(
3637 store_root_hash,
3638 candidate_family,
3639 circle_id,
3640 &control,
3641 leaves,
3642 proofs,
3643 signer,
3644 );
3645 Ok(Self {
3646 circle_id,
3647 epoch_id,
3648 keyring: keyring.to_string(),
3649 roster: current_roster.clone(),
3650 policy: policy_objects,
3651 metadata,
3652 close_intent: None,
3653 close_finalization: None,
3654 close_cancellation: None,
3655 access,
3656 control,
3657 })
3658 }
3659
3660 #[allow(clippy::too_many_arguments)]
3675 pub(crate) fn resolve(
3676 candidate_family: super::store_commit::CandidateFamilyId,
3677 device_id: &str,
3678 store_membership: StoreMembershipStateRef,
3679 membership_authority: MembershipGrantCreationAuthority,
3680 store_members: Vec<(String, MemberRole)>,
3681 chosen_control: &PreparedCircleControl,
3682 chosen_roster: &CircleMaterializedRoster,
3683 chosen_metadata: &CircleMetadata,
3684 keyring: &str,
3685 losing_branches: Vec<ResolvedConflictBranch>,
3686 ids: &dyn crate::id_provider::IdProvider,
3687 signer: &UserKeypair,
3688 ) -> Result<Self, CircleTransitionError> {
3689 let context = circle_successor_context(
3690 store_members,
3691 chosen_control,
3692 chosen_roster,
3693 chosen_metadata,
3694 keyring,
3695 signer,
3696 )?;
3697 let CircleSuccessorContext {
3698 store_members,
3699 author_pubkey,
3700 epoch: active_epoch,
3701 grant_id: _,
3702 author_authority,
3703 key_fingerprint,
3704 } = context;
3705 let store_root_hash = chosen_control.value.store_root_hash;
3706 let circle_id = chosen_control.value.circle_id;
3707 let epoch_id = chosen_control.value.epoch_id();
3708 let roster_state = chosen_control.value.roster_state_ref();
3709
3710 let mut covered_control_heads = active_epoch.covered_control_heads.clone();
3718 let mut metadata = active_epoch.metadata.clone();
3719 let mut roster = active_epoch.roster.clone();
3720 for branch in &losing_branches {
3721 merge_frontier_head(
3722 &mut covered_control_heads,
3723 branch.control_head.clone(),
3724 |head| head.coord.stream_key(),
3725 |head| head.coord.seq,
3726 );
3727 for head in &branch.metadata_heads {
3728 merge_frontier_head(
3729 &mut metadata.heads,
3730 head.clone(),
3731 |head| head.coord.stream_key(),
3732 |head| head.coord.seq,
3733 );
3734 }
3735 for head in &branch.roster_heads {
3736 merge_frontier_head(
3737 &mut roster.heads,
3738 head.clone(),
3739 |head| head.coord.stream_key(),
3740 |head| head.coord.seq,
3741 );
3742 }
3743 }
3744 covered_control_heads.sort_by_key(|head| head.coord.stream_key());
3745 metadata.heads.sort_by_key(|head| head.coord.stream_key());
3746 roster.heads.sort_by_key(|head| head.coord.stream_key());
3747
3748 let selected_metadata = std::iter::once(chosen_metadata)
3753 .chain(
3754 losing_branches
3755 .iter()
3756 .map(|branch| &branch.selected_metadata),
3757 )
3758 .max_by_key(|entry| {
3759 (
3760 entry.metadata_stamp.clone(),
3761 entry.author_pubkey.clone(),
3762 entry.device_id.clone(),
3763 entry.metadata_hash(),
3764 )
3765 })
3766 .expect("a resolution names at least the chosen branch's metadata")
3767 .clone();
3768 metadata.selected = selected_metadata.coord();
3769 metadata.state_hash = selected_metadata.metadata_hash();
3770
3771 let mut control_value = CircleControl {
3772 version: STORE_PROTOCOL_VERSION,
3773 store_root_hash,
3774 circle_id,
3775 value: CircleControlValue {
3776 order: MergeCircleControlOrder {
3777 device_id: device_id.to_string(),
3778 stream_id: active_epoch
3779 .covered_control_heads
3780 .iter()
3781 .find(|head| head.coord.stream_key().author_pubkey == author_pubkey)
3782 .map_or_else(
3783 || {
3784 AuthorStreamId::from_digest(generated_id_digest(
3785 ids,
3786 b"coven.circle-transition-draft-stream.v1\0",
3787 ))
3788 },
3789 |head| head.coord.stream_id,
3790 ),
3791 author_owner_grant: chosen_metadata.author_owner_grant.clone(),
3792 seq: chosen_control
3793 .value
3794 .ordinal()
3795 .checked_add(1)
3796 .ok_or(CircleTransitionError::SequenceOverflow)?,
3797 previous_control_hash: Some(chosen_control.coord.control_hash()),
3798 dependencies: vec![chosen_control.coord.clone()],
3799 },
3800 state: CircleControlState::ActiveEpoch(MergeActiveCircleEpoch {
3801 common: active_epoch.common.clone(),
3802 metadata,
3803 roster,
3804 store_membership,
3805 covered_control_heads,
3806 }),
3807 author_authority,
3808 membership_authority,
3809 },
3810 author_pubkey,
3811 signature: String::new(),
3812 };
3813 let leaves = prepare_access_material(
3814 store_root_hash,
3815 candidate_family,
3816 circle_id,
3817 epoch_id,
3818 keyring,
3819 key_fingerprint,
3820 &roster_state,
3821 &chosen_roster.members(),
3822 &control_value
3823 .value
3824 .state
3825 .active_epoch()
3826 .expect("control resolution constructs an active epoch")
3827 .store_membership,
3828 &store_members,
3829 &std::collections::BTreeMap::new(),
3830 ids,
3831 signer,
3832 )?;
3833 let leaf_hashes = leaves.iter().map(|leaf| leaf.leaf_hash).collect::<Vec<_>>();
3834 let (access_root, proofs) = merkle_root_and_proofs(&leaf_hashes);
3835 control_value
3836 .value
3837 .state
3838 .active_epoch_mut()
3839 .expect("control resolution constructs an active epoch")
3840 .common
3841 .access_root = access_root;
3842 control_value.signature = keys::sign_hex(signer, &control_value.canonical_bytes()).1;
3843 let control = PreparedCircleControl {
3844 coord: control_value.coord(),
3845 bytes: serde_json::to_vec(&control_value)
3846 .expect("circle control serialization cannot fail"),
3847 value: control_value,
3848 };
3849 let access = prepare_access_envelopes(
3850 store_root_hash,
3851 candidate_family,
3852 circle_id,
3853 &control,
3854 leaves,
3855 proofs,
3856 signer,
3857 );
3858 Ok(Self {
3859 circle_id,
3860 epoch_id,
3861 keyring: keyring.to_string(),
3862 roster: chosen_roster.clone(),
3863 policy: CircleTransitionDraftPolicy {
3864 roster: CircleRosterDraftPolicy::Inherited,
3865 metadata_successor: false,
3866 },
3867 metadata: selected_metadata,
3868 close_intent: None,
3869 close_finalization: None,
3870 close_cancellation: None,
3871 access,
3872 control,
3873 })
3874 }
3875
3876 #[allow(clippy::too_many_arguments)]
3882 pub(crate) fn delete(
3883 device_id: &str,
3884 store_membership: StoreMembershipStateRef,
3885 membership_authority: MembershipGrantCreationAuthority,
3886 store_members: Vec<(String, MemberRole)>,
3887 current_control: &PreparedCircleControl,
3888 current_roster: &CircleMaterializedRoster,
3889 current_metadata: &CircleMetadata,
3890 keyring: &str,
3891 ids: &dyn crate::id_provider::IdProvider,
3892 signer: &UserKeypair,
3893 ) -> Result<Self, CircleTransitionError> {
3894 let context = circle_delete_successor_context(
3895 store_members,
3896 current_control,
3897 current_roster,
3898 current_metadata,
3899 keyring,
3900 signer,
3901 )?;
3902 let CircleSuccessorContext {
3903 store_members: _,
3904 author_pubkey,
3905 epoch,
3906 grant_id,
3907 author_authority,
3908 key_fingerprint: _,
3909 } = context;
3910 let store_root_hash = current_control.value.store_root_hash;
3911 let circle_id = current_control.value.circle_id;
3912 let epoch_id = current_control.value.epoch_id();
3913 let frozen_epoch = MergeActiveCircleEpoch {
3914 common: epoch.common.clone(),
3915 metadata: epoch.metadata.clone(),
3916 roster: epoch.roster.clone(),
3917 store_membership,
3918 covered_control_heads: epoch.covered_control_heads.clone(),
3919 };
3920 let stream_id = epoch
3921 .covered_control_heads
3922 .iter()
3923 .find(|head| head.coord.stream_key().author_pubkey == author_pubkey)
3924 .map_or_else(
3925 || {
3926 AuthorStreamId::from_digest(generated_id_digest(
3927 ids,
3928 b"coven.circle-transition-draft-stream.v1\0",
3929 ))
3930 },
3931 |head| head.coord.stream_id,
3932 );
3933 let mut control_value = CircleControl {
3934 version: STORE_PROTOCOL_VERSION,
3935 store_root_hash,
3936 circle_id,
3937 value: CircleControlValue {
3938 order: MergeCircleControlOrder {
3939 device_id: device_id.to_string(),
3940 stream_id,
3941 author_owner_grant: grant_id,
3942 seq: current_control
3943 .value
3944 .ordinal()
3945 .checked_add(1)
3946 .ok_or(CircleTransitionError::SequenceOverflow)?,
3947 previous_control_hash: Some(current_control.coord.control_hash()),
3948 dependencies: vec![current_control.coord.clone()],
3949 },
3950 state: CircleControlState::Deleted(DeletedCircle { frozen_epoch }),
3951 author_authority,
3952 membership_authority,
3953 },
3954 author_pubkey,
3955 signature: String::new(),
3956 };
3957 control_value.signature = keys::sign_hex(signer, &control_value.canonical_bytes()).1;
3958 let control = PreparedCircleControl {
3959 coord: control_value.coord(),
3960 bytes: serde_json::to_vec(&control_value)
3961 .expect("circle control serialization cannot fail"),
3962 value: control_value,
3963 };
3964 Ok(Self {
3965 circle_id,
3966 epoch_id,
3967 keyring: keyring.to_string(),
3968 roster: current_roster.clone(),
3969 policy: CircleTransitionDraftPolicy {
3970 roster: CircleRosterDraftPolicy::Inherited,
3971 metadata_successor: false,
3972 },
3973 metadata: current_metadata.clone(),
3974 close_intent: None,
3975 close_finalization: None,
3976 close_cancellation: None,
3977 access: Vec::new(),
3978 control,
3979 })
3980 }
3981}
3982
3983#[derive(Debug, Clone, Copy)]
3984pub enum CircleSemanticSlot<'a> {
3985 Control {
3986 circle_id: CircleId,
3987 control: &'a CircleControlCoord,
3988 },
3989 ControlHead {
3990 circle_id: CircleId,
3991 control: &'a CircleControlCoord,
3992 head_hash: ObjectHash,
3993 },
3994 RosterEntry {
3995 circle_id: CircleId,
3996 coord: &'a super::circle_roster::CircleRosterCoord,
3997 },
3998 RosterHead {
3999 circle_id: CircleId,
4000 head: &'a CircleRosterHeadRef,
4001 },
4002 RosterResolution {
4003 circle_id: CircleId,
4004 resolution: &'a super::circle_roster::CircleRosterConflictResolutionRef,
4005 },
4006 MetadataEntry {
4007 circle_id: CircleId,
4008 coord: &'a CircleMetadataCoord,
4009 },
4010 MetadataHead {
4011 circle_id: CircleId,
4012 head: &'a CircleMetadataHeadRef,
4013 },
4014}
4015
4016pub fn circle_semantic_prefix(slot: CircleSemanticSlot<'_>) -> String {
4017 match slot {
4018 CircleSemanticSlot::Control { circle_id, control } => format!(
4019 "circle-control/{}/merge/entries/{author_pubkey}/{device_id}/{author_owner_grant}/{stream_id}/{seq}/{control_hash}",
4020 circle_id,
4021 author_pubkey = control.author_pubkey,
4022 device_id = control.device_id,
4023 author_owner_grant = control.author_owner_grant,
4024 stream_id = control.stream_id,
4025 seq = control.seq,
4026 control_hash = control.control_hash,
4027 ),
4028 CircleSemanticSlot::ControlHead {
4029 circle_id,
4030 control,
4031 head_hash: _,
4032 } => {
4033 circle_control_head_prefix(
4034 circle_id,
4035 &CircleAuthorStreamKey {
4036 author_pubkey: control.author_pubkey.clone(),
4037 device_id: control.device_id.clone(),
4038 stream_id: control.stream_id,
4039 author_owner_grant: control.author_owner_grant.clone(),
4040 },
4041 control.seq,
4042 )
4043 }
4044 CircleSemanticSlot::RosterEntry { circle_id, coord } => format!(
4045 "circles/{circle_id}/roster/entries/{}/{}/{}/{}/{}/{}",
4046 coord.author_pubkey,
4047 coord.device_id,
4048 coord.author_owner_grant,
4049 coord.stream_id,
4050 coord.seq,
4051 coord.entry_hash
4052 ),
4053 CircleSemanticSlot::RosterHead { circle_id, head } => {
4054 circle_roster_head_prefix(circle_id, &head.coord.stream_key(), head.coord.seq)
4055 }
4056 CircleSemanticSlot::RosterResolution {
4057 circle_id,
4058 resolution,
4059 } => format!(
4060 "circles/{circle_id}/roster/resolutions/{}/{}/{}",
4061 resolution.conflict_hash,
4062 resolution.resolver_pubkey,
4063 resolution.resolution_hash
4064 ),
4065 CircleSemanticSlot::MetadataEntry { circle_id, coord } => format!(
4066 "circles/{circle_id}/metadata/entries/{}/{}/{}/{}/{}/{}",
4067 coord.author_pubkey,
4068 coord.device_id,
4069 coord.author_owner_grant,
4070 coord.stream_id,
4071 coord.seq,
4072 coord.metadata_hash
4073 ),
4074 CircleSemanticSlot::MetadataHead { circle_id, head } => {
4075 circle_metadata_head_prefix(circle_id, &head.coord.stream_key(), head.coord.seq)
4076 }
4077 }
4078}
4079
4080pub fn circle_control_head_prefix(
4081 circle_id: CircleId,
4082 stream: &CircleAuthorStreamKey,
4083 seq: u64,
4084) -> String {
4085 format!(
4086 "circle-control/{circle_id}/merge/heads/{}/{}/{}/{}/{seq}",
4087 stream.author_pubkey, stream.device_id, stream.author_owner_grant, stream.stream_id
4088 )
4089}
4090
4091pub fn circle_roster_head_prefix(
4092 circle_id: CircleId,
4093 stream: &CircleAuthorStreamKey,
4094 seq: u64,
4095) -> String {
4096 format!(
4097 "circles/{circle_id}/roster/heads/{}/{}/{}/{}/{seq}",
4098 stream.author_pubkey, stream.device_id, stream.author_owner_grant, stream.stream_id
4099 )
4100}
4101
4102pub fn circle_metadata_head_prefix(
4103 circle_id: CircleId,
4104 stream: &CircleAuthorStreamKey,
4105 seq: u64,
4106) -> String {
4107 format!(
4108 "circles/{circle_id}/metadata/heads/{}/{}/{}/{}/{seq}",
4109 stream.author_pubkey, stream.device_id, stream.author_owner_grant, stream.stream_id
4110 )
4111}
4112
4113pub fn circle_epoch_close_outcome_semantic_prefix(
4114 circle_id: CircleId,
4115 close_id: CircleEpochCloseId,
4116) -> String {
4117 format!("circles/{circle_id}/epoch-close/{close_id}/outcome")
4118}
4119
4120pub fn circle_epoch_close_intent_semantic_prefix(
4121 circle_id: CircleId,
4122 close_id: CircleEpochCloseId,
4123 intent_hash: ObjectHash,
4124) -> String {
4125 format!("circles/{circle_id}/epoch-close/{close_id}/intent/{intent_hash}")
4126}
4127
4128pub fn circle_epoch_close_response_semantic_prefix(
4129 circle_id: CircleId,
4130 close_id: CircleEpochCloseId,
4131 device_id: super::store_commit::StoreDeviceId,
4132) -> String {
4133 format!("circles/{circle_id}/epoch-close/{close_id}/responses/{device_id}")
4134}
4135
4136pub fn verify_circle_semantic_prefix(
4137 actual: &str,
4138 slot: CircleSemanticSlot<'_>,
4139) -> Result<(), CircleSemanticPathError> {
4140 let expected = circle_semantic_prefix(slot);
4141 if actual == expected {
4142 Ok(())
4143 } else {
4144 Err(CircleSemanticPathError {
4145 expected,
4146 actual: actual.to_string(),
4147 })
4148 }
4149}
4150
4151#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
4152#[error("Circle object path {actual:?} does not match signed coordinate path {expected:?}")]
4153pub struct CircleSemanticPathError {
4154 pub expected: String,
4155 pub actual: String,
4156}
4157
4158pub fn recipient_slot(
4159 owner: &UserKeypair,
4160 recipient_pubkey: &str,
4161 circle_id: CircleId,
4162) -> Result<String, CircleTransitionError> {
4163 recipient_slot_with_peer(owner, recipient_pubkey, circle_id)
4164}
4165
4166pub fn recipient_slot_with_peer(
4167 local_identity: &UserKeypair,
4168 peer_pubkey: &str,
4169 circle_id: CircleId,
4170) -> Result<String, CircleTransitionError> {
4171 let peer_ed25519: [u8; keys::SIGN_PUBLICKEYBYTES] = hex::decode(peer_pubkey)
4172 .map_err(|_| CircleTransitionError::InvalidRecipient(peer_pubkey.to_string()))?
4173 .try_into()
4174 .map_err(|_| CircleTransitionError::InvalidRecipient(peer_pubkey.to_string()))?;
4175 let peer_x25519 = keys::ed25519_to_x25519_public_key(&peer_ed25519)
4176 .map_err(|_| CircleTransitionError::InvalidRecipient(peer_pubkey.to_string()))?;
4177 let shared = keys::x25519_shared_secret(local_identity.to_x25519_secret_key(), peer_x25519)
4178 .map_err(|_| CircleTransitionError::InvalidRecipient(peer_pubkey.to_string()))?;
4179 let mut mac = Hmac::<Sha256>::new_from_slice(&shared).expect("HMAC accepts X25519 output");
4180 mac.update(RECIPIENT_SLOT_DOMAIN);
4181 mac.update(circle_id.as_bytes());
4182 Ok(hex::encode(mac.finalize().into_bytes()))
4183}
4184
4185#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
4186pub enum CircleTransitionError {
4187 #[error("circle name cannot be empty")]
4188 EmptyName,
4189 #[error("circle Store membership reference does not hash the supplied member state")]
4190 MembershipStateMismatch,
4191 #[error("circle creator is not a current Store writer")]
4192 AuthorNotStoreWriter,
4193 #[error("circle operation author is not a current Circle Owner")]
4194 AuthorNotCircleOwner,
4195 #[error("circle operation current state is invalid")]
4196 InvalidCurrentState,
4197 #[error("circle transition sequence overflow")]
4198 SequenceOverflow,
4199 #[error("circle creator Store grant does not match the Store policy")]
4200 MembershipGrantPolicy,
4201 #[error("circle recipient has an invalid Ed25519 public key: {0}")]
4202 InvalidRecipient(String),
4203 #[error("circle member is not a current Store member: {0}")]
4204 MemberNotInStore(String),
4205 #[error("circle roster: {0}")]
4206 Roster(#[from] CircleRosterError),
4207}
4208
4209#[cfg(test)]
4210mod tests {
4211 use super::*;
4212
4213 #[test]
4214 fn semantic_paths_bind_writer_and_author_stream_id() {
4215 let grant = MembershipGrantId(ObjectHash::digest(b"path grant"));
4216 let first = super::super::circle_roster::CircleRosterCoord {
4217 author_pubkey: "owner".to_string(),
4218 device_id: "device".to_string(),
4219 stream_id: AuthorStreamId::from_bytes([1; 32]),
4220 author_owner_grant: grant.clone(),
4221 seq: 1,
4222 entry_hash: ObjectHash::digest(b"entry"),
4223 };
4224 let mut substituted = first.clone();
4225 substituted.stream_id = AuthorStreamId::from_bytes([2; 32]);
4226 let circle_id = CircleId::from_bytes([7; 16]);
4227 let first_path = circle_semantic_prefix(CircleSemanticSlot::RosterEntry {
4228 circle_id,
4229 coord: &first,
4230 });
4231
4232 assert!(first_path.contains(&first.stream_id.to_string()));
4233 assert!(verify_circle_semantic_prefix(
4234 &first_path,
4235 CircleSemanticSlot::RosterEntry {
4236 circle_id,
4237 coord: &substituted,
4238 },
4239 )
4240 .is_err());
4241 }
4242}