1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
4#[serde(deny_unknown_fields)]
5pub struct StorePackageRef {
6 pub candidate_family: CandidateFamilyId,
7 pub content_hash: ObjectHash,
8 pub schema_version: u32,
9 pub changeset_size: u64,
10 pub object: ExactObjectRef,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct CirclePackageRef {
16 pub circle_id: CircleId,
17 pub control: CircleControlCoord,
18 pub package: StorePackageRef,
19 pub key_fingerprint: KeyFingerprint,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct CircleAccessEnvelopeObjectRef {
26 pub owner_pubkey: String,
27 pub recipient_slot: String,
28 pub control_hash: ObjectHash,
29 pub leaf_id: AccessLeafId,
30 pub leaf_hash: ObjectHash,
31 pub object: ExactObjectRef,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct CircleAccessLeafObjectRef {
38 pub owner_pubkey: String,
39 pub epoch_id: CircleEpochId,
40 pub recipient_slot: String,
41 pub leaf_id: AccessLeafId,
42 pub leaf_hash: ObjectHash,
43 pub object: ExactObjectRef,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct CircleAccessObjectRef {
49 pub leaf: CircleAccessLeafObjectRef,
50 pub envelope: CircleAccessEnvelopeObjectRef,
51 pub bootstrap: Option<SnapshotImageRef>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
56#[serde(deny_unknown_fields)]
57pub struct CircleMetadataObjectRef {
58 pub key_fingerprint: KeyFingerprint,
59 pub object: ExactObjectRef,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct CircleActivationObjects {
66 pub control: ExactObjectRef,
67 pub close_intent: Option<crate::circle::CircleEpochCloseIntentRef>,
68 pub close_outcome: Option<crate::circle::CircleEpochCloseOutcomeRef>,
69 pub close_cancellation: Option<crate::circle::CircleEpochCloseCancellationRef>,
70 #[serde(with = "ordered_map_entries")]
71 pub roster_entries: BTreeMap<CircleRosterCoord, ExactObjectRef>,
72 pub roster_heads: Vec<CircleRosterHeadRef>,
73 #[serde(with = "ordered_map_entries")]
74 pub roster_resolutions: BTreeMap<CircleRosterConflictResolutionRef, ExactObjectRef>,
75 #[serde(with = "ordered_map_entries")]
76 pub metadata_entries: BTreeMap<CircleMetadataCoord, CircleMetadataObjectRef>,
77 pub metadata_heads: Vec<CircleMetadataHeadRef>,
78 pub access: Vec<CircleAccessObjectRef>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct CircleControlRef {
84 pub circle_id: CircleId,
85 pub control: CircleControlCoord,
86 pub head_hash: ObjectHash,
87 pub head_object: ExactObjectRef,
88 pub objects: CircleActivationObjects,
89}
90
91impl CircleControlRef {
92 pub fn circle_id(&self) -> CircleId {
93 self.circle_id
94 }
95
96 pub fn control(&self) -> &CircleControlCoord {
97 &self.control
98 }
99
100 pub fn head_hash(&self) -> ObjectHash {
101 self.head_hash
102 }
103
104 pub fn head_object(&self) -> &ExactObjectRef {
105 &self.head_object
106 }
107
108 pub fn objects(&self) -> &CircleActivationObjects {
109 &self.objects
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
114#[serde(deny_unknown_fields)]
115pub struct StoreDeviceRegistrationRef {
116 pub device_id: StoreDeviceId,
117 pub registration_hash: ObjectHash,
118 pub object: ExactObjectRef,
119}
120
121impl StoreDeviceRegistrationRef {
122 pub fn from_registration(
123 registration: &StoreDeviceRegistration,
124 object: ExactObjectRef,
125 ) -> Self {
126 Self {
127 device_id: registration.device_id,
128 registration_hash: registration.registration_hash(),
129 object,
130 }
131 }
132
133 pub fn verify_registration(
134 &self,
135 registration: &StoreDeviceRegistration,
136 ) -> Result<(), StoreProtocolError> {
137 if registration.device_id != self.device_id
138 || registration.registration_hash() != self.registration_hash
139 {
140 return Err(StoreProtocolError::DeviceRegistrationRefMismatch {
141 device_id: self.device_id.to_string(),
142 expected: self.registration_hash,
143 actual: registration.registration_hash(),
144 });
145 }
146 Ok(())
147 }
148}
149
150pub struct CirclePackageInput<'a> {
151 pub circle_id: CircleId,
152 pub control: CircleControlCoord,
153 pub key_fingerprint: KeyFingerprint,
154 pub package: StorePackageInput<'a>,
155}
156
157pub struct StorePackageInput<'a> {
158 pub candidate_family: CandidateFamilyId,
159 pub schema_version: u32,
160 pub bytes: &'a [u8],
161 pub object: ExactObjectRef,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(deny_unknown_fields)]
166pub struct StoreControl {
167 pub transition: crate::membership::MergeMembershipHeadTransition,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
171#[serde(transparent)]
172pub struct OwnerPromotionId(ObjectHash);
173
174impl OwnerPromotionId {
175 pub fn from_generated(value: String) -> Self {
176 Self(ObjectHash::digest(
177 &[
178 b"coven.owner-promotion-id.v1\0".as_slice(),
179 value.as_bytes(),
180 ]
181 .concat(),
182 ))
183 }
184}
185
186impl fmt::Display for OwnerPromotionId {
187 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
188 fmt::Display::fmt(&self.0, formatter)
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(deny_unknown_fields)]
194pub struct OwnerPromotionFinalization {
195 pub author_stream: AuthorStreamId,
196 pub seq: u64,
197 pub previous_hash: Option<ObjectHash>,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(deny_unknown_fields)]
203pub struct OwnerPromotionRequestBody {
204 pub promotion_id: OwnerPromotionId,
205 pub store_root_hash: ObjectHash,
206 pub promoter_registration: StoreDeviceRegistrationRef,
207 pub promoter_owner_grant: MembershipGrantId,
208 pub member_pubkey: String,
209 pub member_grant: MembershipGrantId,
210 pub member_registration: StoreDeviceRegistrationRef,
211 pub intended_owner_grant: MembershipGrantId,
212 pub predecessor_membership: StoreMembershipStateRef,
213 pub predecessor_devices: StoreDeviceStateRef,
214 pub finalization: OwnerPromotionFinalization,
215 pub publication_slot: ObjectSlot,
216}
217
218impl SignedBody for OwnerPromotionRequestBody {
219 const DOMAIN: &'static [u8] = b"coven.owner-promotion-request.v1\0";
220}
221
222pub type OwnerPromotionRequest = Signed<OwnerPromotionRequestBody>;
223
224impl OwnerPromotionRequest {
225 #[allow(clippy::too_many_arguments)]
226 pub fn signed(
227 promotion_id: OwnerPromotionId,
228 root: &StoreRootRef,
229 promoter_registration: StoreDeviceRegistrationRef,
230 promoter: &StoreDeviceRegistration,
231 promoter_owner_grant: MembershipGrantId,
232 member_pubkey: String,
233 member_grant: MembershipGrantId,
234 member_registration: StoreDeviceRegistrationRef,
235 predecessor_membership: StoreMembershipStateRef,
236 predecessor_devices: StoreDeviceStateRef,
237 finalization: OwnerPromotionFinalization,
238 publication_slot: ObjectSlot,
239 signer: &UserKeypair,
240 ) -> Result<Self, StoreProtocolError> {
241 let intended_owner_grant =
242 derive_owner_promotion_grant(root.store_root_hash, promotion_id, &member_pubkey);
243 let body = OwnerPromotionRequestBody {
244 promotion_id,
245 store_root_hash: root.store_root_hash,
246 promoter_registration,
247 promoter_owner_grant,
248 member_pubkey,
249 member_grant,
250 member_registration,
251 intended_owner_grant,
252 predecessor_membership,
253 predecessor_devices,
254 finalization,
255 publication_slot,
256 };
257 body.validate_shape(root, promoter)?;
258 let device_signer = promoter.device_signer(signer)?;
259 Ok(Signed::sign(body, &device_signer))
260 }
261
262 pub fn verify(
263 &self,
264 root: &StoreRootRef,
265 promoter: &StoreDeviceRegistration,
266 ) -> Result<(), StoreProtocolError> {
267 self.body().validate_shape(root, promoter)?;
268 self.verify_by(&promoter.device_signing_pubkey)
269 }
270}
271
272impl OwnerPromotionRequestBody {
273 fn validate_shape(
274 &self,
275 root: &StoreRootRef,
276 promoter: &StoreDeviceRegistration,
277 ) -> Result<(), StoreProtocolError> {
278 self.promoter_registration.verify_registration(promoter)?;
279 crate::objects::verify_store_root(root.store_root_hash, self.store_root_hash)?;
280 if promoter.store_root != *root
281 || promoter.author_pubkey == self.member_pubkey
282 || self.member_pubkey.is_empty()
283 || self.intended_owner_grant
284 != derive_owner_promotion_grant(
285 self.store_root_hash,
286 self.promotion_id,
287 &self.member_pubkey,
288 )
289 || self.finalization.seq == 0
290 || self.publication_slot.logical_key()
291 != format!(
292 "{}.json",
293 owner_promotion_request_publication_semantic_prefix(self.promotion_id)
294 )
295 {
296 return Err(StoreProtocolError::OwnerPromotionMismatch);
297 }
298 Ok(())
299 }
300}
301
302pub(crate) fn derive_owner_promotion_grant(
303 store_root_hash: ObjectHash,
304 promotion_id: OwnerPromotionId,
305 member_pubkey: &str,
306) -> MembershipGrantId {
307 MembershipGrantId(ObjectHash::digest(&domain_json(
308 b"coven.owner-promotion-grant.v1\0",
309 &(store_root_hash, promotion_id, member_pubkey),
310 )))
311}
312
313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
314#[serde(deny_unknown_fields)]
315pub struct OwnerPromotionRequestActivation {
316 pub commit: StoreBatchCommitRef,
317 pub publication: StorePublicationRef,
318}
319
320impl OwnerPromotionRequestActivation {
321 pub fn commit(&self) -> &StoreBatchCommitRef {
322 &self.commit
323 }
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(deny_unknown_fields)]
328pub struct OwnerPromotionAnchors {
329 pub membership: GrantStreamAnchor,
330 pub recovery: GrantStreamAnchor,
331}
332
333impl OwnerPromotionAnchors {
334 pub fn recovery(&self) -> &GrantStreamAnchor {
335 &self.recovery
336 }
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341#[serde(deny_unknown_fields)]
342pub struct OwnerPromotionAcceptanceBody {
343 pub request: Box<OwnerPromotionRequest>,
344 pub activation: OwnerPromotionRequestActivation,
345 pub anchors: OwnerPromotionAnchors,
346}
347
348impl SignedBody for OwnerPromotionAcceptanceBody {
349 const DOMAIN: &'static [u8] = b"coven.owner-promotion-acceptance.v1\0";
350}
351
352pub type OwnerPromotionAcceptance = Signed<OwnerPromotionAcceptanceBody>;
353
354#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
355#[serde(rename_all = "snake_case", deny_unknown_fields)]
356pub enum OwnerPromotionStaleReason {
357 MergeFinalizationPointOccupied { winner: MembershipHeadRef },
358 MergeActivationRejected,
359}
360
361impl OwnerPromotionAcceptance {
362 pub fn signed(
363 request: OwnerPromotionRequest,
364 activation: OwnerPromotionRequestActivation,
365 anchors: OwnerPromotionAnchors,
366 candidate: &StoreDeviceRegistration,
367 signer: &UserKeypair,
368 ) -> Result<Self, StoreProtocolError> {
369 let body = OwnerPromotionAcceptanceBody {
370 request: Box::new(request),
371 activation,
372 anchors,
373 };
374 body.validate_shape(candidate)?;
375 let device_signer = candidate.device_signer(signer)?;
376 Ok(Signed::sign(body, &device_signer))
377 }
378
379 pub fn verify(&self, candidate: &StoreDeviceRegistration) -> Result<(), StoreProtocolError> {
380 self.body().validate_shape(candidate)?;
381 self.verify_by(&candidate.device_signing_pubkey)
382 }
383}
384
385impl OwnerPromotionAcceptanceBody {
386 fn validate_shape(
387 &self,
388 candidate: &StoreDeviceRegistration,
389 ) -> Result<(), StoreProtocolError> {
390 self.request
391 .member_registration
392 .verify_registration(candidate)?;
393 if candidate.store_root.store_root_hash != self.request.store_root_hash
394 || candidate.author_pubkey != self.request.member_pubkey
395 || !matches!(
396 self.anchors.recovery(),
397 GrantStreamAnchor::OwnerRecovery { .. }
398 )
399 {
400 return Err(StoreProtocolError::OwnerPromotionMismatch);
401 }
402 {
403 let membership = &self.anchors.membership;
404 let recovery = &self.anchors.recovery;
405 if !matches!(membership, GrantStreamAnchor::StoreMembership { .. }) {
406 return Err(StoreProtocolError::OwnerPromotionMismatch);
407 }
408 let membership_stream = StreamActivation::grant_authorized_stream_id(
409 self.request.store_root_hash,
410 &self.request.member_registration,
411 &self.request.intended_owner_grant,
412 StreamAnchorDomain::StoreMembership,
413 );
414 let membership_key = format!(
415 "{}.json",
416 membership_head_slot_prefix(
417 &self.request.member_pubkey,
418 &self.request.intended_owner_grant,
419 membership_stream,
420 1,
421 )
422 );
423 let recovery_key = format!(
424 "{}.json",
425 owner_recovery_semantic_prefix(
426 &self.request.member_pubkey,
427 self.request.intended_owner_grant.clone(),
428 1,
429 )
430 );
431 if membership.first_slot().logical_key() != membership_key
432 || recovery.first_slot().logical_key() != recovery_key
433 || matches!(
434 (membership.first_slot().physical(), recovery.first_slot().physical()),
435 (
436 crate::objects::PhysicalObjectLocator::Opaque(left),
437 crate::objects::PhysicalObjectLocator::Opaque(right),
438 ) if left == right
439 )
440 {
441 return Err(StoreProtocolError::OwnerPromotionMismatch);
442 }
443 }
444 Ok(())
445 }
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449#[serde(deny_unknown_fields)]
450pub struct CandidateObjectManifest {
451 pub family: CandidateFamilyId,
452 pub objects: Vec<CandidateExclusiveObjectRef>,
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
456#[serde(rename_all = "snake_case", deny_unknown_fields)]
457pub enum CandidateExclusiveObjectRef {
458 StorePackage(StorePackageRef),
459 CirclePackage(CirclePackageRef),
460 CircleEpochCloseIntent {
461 circle_id: CircleId,
462 reference: crate::circle::CircleEpochCloseIntentRef,
463 },
464 CircleEpochCloseOutcome {
465 circle_id: CircleId,
466 reference: crate::circle::CircleEpochCloseOutcomeRef,
467 },
468 CircleEpochCloseCancellation {
469 circle_id: CircleId,
470 reference: crate::circle::CircleEpochCloseCancellationRef,
471 },
472 CircleAccess {
473 circle_id: CircleId,
474 access: CircleAccessObjectRef,
475 },
476}
477
478#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
479#[serde(rename_all = "snake_case", deny_unknown_fields)]
480pub enum DeviceJoinAttemptDecisionRef {
481 Attempt(DeviceJoinAttemptId),
482 Abandoned(crate::store_commit::DeviceJoinAbandonmentRef),
483}
484
485impl DeviceJoinAttemptDecisionRef {
486 pub fn attempt_id(&self) -> DeviceJoinAttemptId {
487 match self {
488 Self::Attempt(attempt_id) => *attempt_id,
489 Self::Abandoned(reference) => reference.attempt_id,
490 }
491 }
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
495#[serde(deny_unknown_fields)]
496pub struct StoreCommitOperations {
497 pub acknowledgement: Option<StoreAckRef>,
498 pub circle_acknowledgements: Vec<CircleAckRef>,
499 pub control: Option<StoreControl>,
500 pub device_join_attempt_decisions: Vec<DeviceJoinAttemptDecisionRef>,
501 pub provider_access_grants: Vec<crate::provider::StoreMemberProviderAccessGrantRef>,
502 pub device_registrations: Vec<ActivatedStoreDeviceRegistrationRef>,
503 pub device_exclusion_proposals: Vec<StoreDeviceExclusionProposalRef>,
504 pub device_exclusion_outcomes: Vec<StoreDeviceExclusionOutcomeRef>,
505 pub stream_activations: Vec<StreamActivation>,
506 pub circle_controls: Vec<CircleControlRef>,
507 pub store_package: Option<StorePackageRef>,
508 pub circle_packages: Vec<CirclePackageRef>,
509}
510
511impl StoreCommitOperations {
512 pub fn is_acknowledgement_only(&self) -> bool {
515 self.acknowledgement.is_some() && self.has_no_non_acknowledgement_operations()
516 }
517
518 pub(super) fn is_empty(&self) -> bool {
519 self.acknowledgement.is_none()
520 && self.circle_acknowledgements.is_empty()
521 && self.has_no_non_acknowledgement_operations()
522 }
523
524 pub fn is_circle_control_activation_only(&self) -> bool {
525 self.acknowledgement.is_none()
526 && self.circle_acknowledgements.is_empty()
527 && self.control.is_none()
528 && self.device_join_attempt_decisions.is_empty()
529 && self.provider_access_grants.is_empty()
530 && self.device_registrations.is_empty()
531 && self.device_exclusion_proposals.is_empty()
532 && self.device_exclusion_outcomes.is_empty()
533 && self.circle_controls.len() == 1
534 && self.store_package.is_none()
535 && self.circle_packages.is_empty()
536 }
537
538 fn has_no_non_acknowledgement_operations(&self) -> bool {
539 let Self {
540 acknowledgement: _,
541 circle_acknowledgements: _,
542 control,
543 device_join_attempt_decisions,
544 provider_access_grants,
545 device_registrations,
546 device_exclusion_proposals,
547 device_exclusion_outcomes,
548 stream_activations,
549 circle_controls,
550 store_package,
551 circle_packages,
552 } = self;
553 control.is_none()
554 && device_join_attempt_decisions.is_empty()
555 && provider_access_grants.is_empty()
556 && device_registrations.is_empty()
557 && device_exclusion_proposals.is_empty()
558 && device_exclusion_outcomes.is_empty()
559 && stream_activations.is_empty()
560 && circle_controls.is_empty()
561 && store_package.is_none()
562 && circle_packages.is_empty()
563 }
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
567#[serde(rename_all = "snake_case", deny_unknown_fields)]
568pub enum StoreCommitBody {
569 Operations(StoreCommitOperations),
570 ReclaimAuthorization {
571 authorization: Box<crate::reclaim::ReclaimAuthorizationRef>,
572 },
573 ReclaimReceipt {
574 receipt: Box<crate::reclaim::ReclaimReceiptRef>,
575 },
576 OwnerPromotionRequest {
577 request: Box<OwnerPromotionRequest>,
578 },
579 AbandonCandidates {
580 manifests: Vec<CandidateCleanupManifest>,
581 },
582}
583
584pub struct StoreCommitOperationsInput<'a> {
585 pub acknowledgement: Option<StoreAckRef>,
586 pub circle_acknowledgements: Vec<CircleAckRef>,
587 pub control: Option<StoreControl>,
588 pub device_join_attempt_decisions: Vec<DeviceJoinAttemptDecisionRef>,
589 pub provider_access_grants: Vec<crate::provider::StoreMemberProviderAccessGrantRef>,
590 pub device_registrations: Vec<ActivatedStoreDeviceRegistrationRef>,
591 pub device_exclusion_proposals: Vec<StoreDeviceExclusionProposalRef>,
592 pub device_exclusion_outcomes: Vec<StoreDeviceExclusionOutcomeRef>,
593 pub stream_activations: Vec<StreamActivation>,
594 pub circle_controls: Vec<CircleControlRef>,
595 pub store_package: Option<StorePackageInput<'a>>,
596 pub circle_packages: &'a [CirclePackageInput<'a>],
597}
598
599impl StoreCommitOperationsInput<'_> {
600 pub fn empty() -> StoreCommitOperationsInput<'static> {
602 StoreCommitOperationsInput {
603 acknowledgement: None,
604 circle_acknowledgements: Vec::new(),
605 control: None,
606 device_join_attempt_decisions: Vec::new(),
607 provider_access_grants: Vec::new(),
608 device_registrations: Vec::new(),
609 device_exclusion_proposals: Vec::new(),
610 device_exclusion_outcomes: Vec::new(),
611 stream_activations: Vec::new(),
612 circle_controls: Vec::new(),
613 store_package: None,
614 circle_packages: &[],
615 }
616 }
617}