Skip to main content

coven_protocol/store_commit/
packages.rs

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/// Exact recipient-visible access envelope paired with its sealed leaf.
23#[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/// Exact recipient-sealed access-leaf object named by a Store activation.
35#[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/// Exact Circle-metadata object and the epoch key that must open it.
55#[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/// Closed exact object graph needed to verify one Store-activated Circle control.
63#[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/// The wire body of an owner-promotion request. Every field here is signed.
201#[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/// The wire body of a promoted member's acceptance. Every field here is signed.
340#[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    /// Acknowledgements report observed state without introducing new edits or
513    /// authority. Receiving them alone does not require an acknowledgement.
514    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    /// An input carrying no operations; authors fill in the kinds they commit.
601    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}