Skip to main content

coven_protocol/store_commit/
registration.rs

1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4#[serde(deny_unknown_fields)]
5pub struct DeviceRecoveryReadiness {
6    pub registration: StoreDeviceRegistrationRef,
7    pub initial_ack: StoreAckRef,
8    pub bootstrap_cut: StoreHistoryCut,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(deny_unknown_fields)]
13pub struct OwnerRecoveryNodeBody {
14    pub store_root_hash: ObjectHash,
15    pub recovery_id: DeviceRecoveryId,
16    pub owner_pubkey: String,
17    pub owner_grant: MembershipGrantId,
18    pub sequence: u64,
19    pub membership: StoreMembershipStateRef,
20    pub predecessor: Option<OwnerRecoveryNodeRef>,
21    pub readiness: DeviceRecoveryReadiness,
22    pub next_slot: ObjectSlot,
23}
24
25impl SignedBody for OwnerRecoveryNodeBody {
26    const DOMAIN: &'static [u8] = OWNER_RECOVERY_NODE_DOMAIN;
27}
28
29pub type OwnerRecoveryNode = Signed<OwnerRecoveryNodeBody>;
30
31impl OwnerRecoveryNode {
32    #[allow(clippy::too_many_arguments)]
33    pub fn signed(
34        store_root_hash: ObjectHash,
35        recovery_id: DeviceRecoveryId,
36        owner_grant: MembershipGrantId,
37        sequence: u64,
38        membership: StoreMembershipStateRef,
39        predecessor: Option<OwnerRecoveryNodeRef>,
40        readiness: DeviceRecoveryReadiness,
41        next_slot: ObjectSlot,
42        owner_signer: &UserKeypair,
43    ) -> Result<Self, StoreProtocolError> {
44        let body = OwnerRecoveryNodeBody {
45            store_root_hash,
46            recovery_id,
47            owner_pubkey: keys::public_key_hex(owner_signer),
48            owner_grant,
49            sequence,
50            membership,
51            predecessor,
52            readiness,
53            next_slot,
54        };
55        body.validate_shape()?;
56        Ok(Signed::sign(body, owner_signer))
57    }
58
59    pub fn parse_at(
60        bytes: &[u8],
61        store_root: &StoreRootRef,
62        reference: &OwnerRecoveryNodeRef,
63    ) -> Result<Self, StoreProtocolError> {
64        let node: Self = crate::objects::decode_protocol_object(bytes)?;
65        node.body().validate_shape()?;
66        if node.store_root_hash != store_root.store_root_hash
67            || node.owner_pubkey != reference.owner_pubkey
68            || node.owner_grant != reference.owner_grant
69            || node.sequence != reference.sequence
70            || node.node_hash() != reference.node_hash
71        {
72            return Err(StoreProtocolError::OwnerRecoveryMismatch);
73        }
74        let owner_pubkey = node.owner_pubkey.clone();
75        node.verify_by(&owner_pubkey)?;
76        Ok(node)
77    }
78
79    pub fn node_hash(&self) -> ObjectHash {
80        self.hash()
81    }
82}
83
84impl OwnerRecoveryNodeBody {
85    fn validate_shape(&self) -> Result<(), StoreProtocolError> {
86        let predecessor_matches = match &self.predecessor {
87            None => self.sequence == 1,
88            Some(predecessor) => {
89                predecessor.owner_pubkey == self.owner_pubkey
90                    && predecessor.owner_grant == self.owner_grant
91                    && predecessor.sequence.checked_add(1) == Some(self.sequence)
92            }
93        };
94        if !predecessor_matches || self.readiness.initial_ack.sequence != 1 {
95            return Err(StoreProtocolError::OwnerRecoveryMismatch);
96        }
97        Ok(())
98    }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case", deny_unknown_fields)]
103pub enum StoreDeviceRegistrationOrigin {
104    Founder {
105        creation_id: StoreCreationId,
106    },
107    Join {
108        attempt_id: DeviceJoinAttemptId,
109    },
110    Recovery {
111        recovery_id: DeviceRecoveryId,
112        recovery_slot: ObjectSlot,
113        owner_grant: MembershipGrantId,
114    },
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "snake_case", deny_unknown_fields)]
119pub enum StoreDeviceRegistrationActivation {
120    Founder {
121        root: StoreRootRef,
122    },
123    Join {
124        attempt_id: DeviceJoinAttemptId,
125    },
126    Recovery {
127        recovery_id: DeviceRecoveryId,
128        node: OwnerRecoveryNodeRef,
129    },
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(deny_unknown_fields)]
134pub struct ActivatedStoreDeviceRegistrationRef {
135    pub registration: StoreDeviceRegistrationRef,
136    pub authority: StoreDeviceRegistrationActivationRef,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "snake_case", deny_unknown_fields)]
141pub enum StoreDeviceRegistrationActivationRef {
142    Join {
143        attempt_id: DeviceJoinAttemptId,
144    },
145    Recovery {
146        recovery_id: DeviceRecoveryId,
147        node: OwnerRecoveryNodeRef,
148    },
149}
150
151impl StoreDeviceRegistrationOrigin {
152    pub(super) fn external_id(&self) -> ObjectHash {
153        match self {
154            Self::Founder { creation_id } => creation_id.object_hash(),
155            Self::Join { attempt_id, .. } => attempt_id.object_hash(),
156            Self::Recovery { recovery_id, .. } => recovery_id.object_hash(),
157        }
158    }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
162#[serde(rename_all = "snake_case", deny_unknown_fields)]
163pub enum DeviceStreamAnchor {
164    StoreAnnouncements {
165        first_slot: ObjectSlot,
166    },
167    StoreAcknowledgements {
168        first_slot: ObjectSlot,
169    },
170    StoreSnapshots {
171        first_slot: ObjectSlot,
172    },
173    /// Per-(device, Circle) acknowledgement stream. Unlike the three permanent
174    /// anchors above, this is never a registration field: it is derived on
175    /// demand to bind one device's Circle-acknowledgement stream to its Circle.
176    CircleAcknowledgements {
177        circle_id: CircleId,
178        first_slot: ObjectSlot,
179    },
180    /// Per-(device, Circle) snapshot stream. Like the Circle-acknowledgement
181    /// anchor, never a registration field: derived on demand to bind one
182    /// device's Circle-snapshot stream to its Circle.
183    CircleSnapshots {
184        circle_id: CircleId,
185        first_slot: ObjectSlot,
186    },
187}
188
189impl DeviceStreamAnchor {
190    pub fn first_slot(&self) -> &ObjectSlot {
191        match self {
192            Self::StoreAnnouncements { first_slot }
193            | Self::StoreAcknowledgements { first_slot }
194            | Self::StoreSnapshots { first_slot }
195            | Self::CircleAcknowledgements { first_slot, .. }
196            | Self::CircleSnapshots { first_slot, .. } => first_slot,
197        }
198    }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
202#[serde(rename_all = "snake_case", deny_unknown_fields)]
203pub enum GrantStreamAnchor {
204    StoreMembership {
205        first_slot: ObjectSlot,
206    },
207    OwnerRecovery {
208        first_slot: ObjectSlot,
209    },
210    CircleControl {
211        circle_id: CircleId,
212        first_slot: ObjectSlot,
213    },
214    CircleRoster {
215        circle_id: CircleId,
216        first_slot: ObjectSlot,
217    },
218    CircleMetadata {
219        circle_id: CircleId,
220        first_slot: ObjectSlot,
221    },
222}
223
224impl GrantStreamAnchor {
225    pub fn first_slot(&self) -> &ObjectSlot {
226        match self {
227            Self::StoreMembership { first_slot }
228            | Self::OwnerRecovery { first_slot }
229            | Self::CircleControl { first_slot, .. }
230            | Self::CircleRoster { first_slot, .. }
231            | Self::CircleMetadata { first_slot, .. } => first_slot,
232        }
233    }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(deny_unknown_fields)]
238pub struct StoreDeviceRegistrationBody {
239    pub store_root: StoreRootRef,
240    pub device_id: StoreDeviceId,
241    pub author_pubkey: String,
242    pub device_signing_pubkey: String,
243    pub origin: StoreDeviceRegistrationOrigin,
244    pub provider: ProviderDeviceBinding,
245    pub store_commits: DeviceStreamAnchor,
246    pub acknowledgements: DeviceStreamAnchor,
247    pub snapshots: DeviceStreamAnchor,
248}
249
250impl SignedBody for StoreDeviceRegistrationBody {
251    const DOMAIN: &'static [u8] = REGISTRATION_DOMAIN;
252}
253
254pub type StoreDeviceRegistration = Signed<StoreDeviceRegistrationBody>;
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
257pub struct ReferencedStoreDeviceRegistration {
258    reference: StoreDeviceRegistrationRef,
259    value: StoreDeviceRegistration,
260}
261
262impl ReferencedStoreDeviceRegistration {
263    pub fn verified(
264        reference: StoreDeviceRegistrationRef,
265        value: StoreDeviceRegistration,
266    ) -> Result<Self, StoreProtocolError> {
267        reference.verify_registration(&value)?;
268        Ok(Self { reference, value })
269    }
270
271    pub fn reference(&self) -> &StoreDeviceRegistrationRef {
272        &self.reference
273    }
274
275    pub fn value(&self) -> &StoreDeviceRegistration {
276        &self.value
277    }
278}
279
280impl<'de> Deserialize<'de> for ReferencedStoreDeviceRegistration {
281    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
282    where
283        D: serde::Deserializer<'de>,
284    {
285        #[derive(Deserialize)]
286        #[serde(deny_unknown_fields)]
287        struct EncodedRegistration {
288            reference: StoreDeviceRegistrationRef,
289            value: StoreDeviceRegistration,
290        }
291
292        let encoded = EncodedRegistration::deserialize(deserializer)?;
293        Self::verified(encoded.reference, encoded.value).map_err(serde::de::Error::custom)
294    }
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
298pub struct ActivatedStoreDeviceRegistration {
299    registration: ReferencedStoreDeviceRegistration,
300    activation: StoreDeviceRegistrationActivation,
301}
302
303impl ActivatedStoreDeviceRegistration {
304    pub fn verified(
305        registration: ReferencedStoreDeviceRegistration,
306        activation: StoreDeviceRegistrationActivation,
307    ) -> Result<Self, StoreProtocolError> {
308        let value = registration.value();
309        let matches = match (&value.origin, &activation) {
310            (
311                StoreDeviceRegistrationOrigin::Founder { .. },
312                StoreDeviceRegistrationActivation::Founder { root },
313            ) => &value.store_root == root,
314            (
315                StoreDeviceRegistrationOrigin::Join {
316                    attempt_id: origin_attempt,
317                    ..
318                },
319                StoreDeviceRegistrationActivation::Join { attempt_id },
320            ) => origin_attempt == attempt_id,
321            (
322                StoreDeviceRegistrationOrigin::Recovery {
323                    recovery_id: origin_recovery,
324                    recovery_slot,
325                    ..
326                },
327                StoreDeviceRegistrationActivation::Recovery { recovery_id, node },
328            ) => origin_recovery == recovery_id && recovery_slot == node.slot(),
329            _ => false,
330        };
331        if !matches {
332            return Err(StoreProtocolError::DeviceStateMismatch);
333        }
334        Ok(Self {
335            registration,
336            activation,
337        })
338    }
339
340    pub fn verify_reference(
341        &self,
342        reference: &ActivatedStoreDeviceRegistrationRef,
343    ) -> Result<(), StoreProtocolError> {
344        if self.registration.reference() != &reference.registration {
345            return Err(StoreProtocolError::DeviceStateMismatch);
346        }
347        let matches = match (&reference.authority, &self.activation) {
348            (
349                StoreDeviceRegistrationActivationRef::Join { attempt_id },
350                StoreDeviceRegistrationActivation::Join {
351                    attempt_id: activated_attempt,
352                },
353            ) => attempt_id == activated_attempt,
354            (
355                StoreDeviceRegistrationActivationRef::Recovery { recovery_id, node },
356                StoreDeviceRegistrationActivation::Recovery {
357                    recovery_id: activated_recovery,
358                    node: activated_node,
359                },
360            ) => recovery_id == activated_recovery && node == activated_node,
361            _ => false,
362        };
363        if !matches {
364            return Err(StoreProtocolError::DeviceStateMismatch);
365        }
366        Ok(())
367    }
368
369    pub fn activated_reference(
370        &self,
371    ) -> Result<ActivatedStoreDeviceRegistrationRef, StoreProtocolError> {
372        let authority = match &self.activation {
373            StoreDeviceRegistrationActivation::Founder { .. } => {
374                return Err(StoreProtocolError::DeviceStateMismatch)
375            }
376            StoreDeviceRegistrationActivation::Join { attempt_id } => {
377                StoreDeviceRegistrationActivationRef::Join {
378                    attempt_id: *attempt_id,
379                }
380            }
381            StoreDeviceRegistrationActivation::Recovery { recovery_id, node } => {
382                StoreDeviceRegistrationActivationRef::Recovery {
383                    recovery_id: *recovery_id,
384                    node: node.clone(),
385                }
386            }
387        };
388        Ok(ActivatedStoreDeviceRegistrationRef {
389            registration: self.registration.reference().clone(),
390            authority,
391        })
392    }
393
394    pub fn registration(&self) -> &ReferencedStoreDeviceRegistration {
395        &self.registration
396    }
397
398    pub fn reference(&self) -> &StoreDeviceRegistrationRef {
399        self.registration.reference()
400    }
401
402    pub fn value(&self) -> &StoreDeviceRegistration {
403        self.registration.value()
404    }
405
406    pub fn activation(&self) -> &StoreDeviceRegistrationActivation {
407        &self.activation
408    }
409
410    pub(crate) fn recovery_cursor(
411        &self,
412    ) -> Result<Option<OwnerRecoveryCursor>, StoreProtocolError> {
413        match (&self.registration.value().origin, &self.activation) {
414            (
415                StoreDeviceRegistrationOrigin::Recovery {
416                    recovery_id,
417                    recovery_slot,
418                    owner_grant,
419                },
420                StoreDeviceRegistrationActivation::Recovery {
421                    recovery_id: activated_recovery_id,
422                    node,
423                },
424            ) if recovery_id == activated_recovery_id
425                && recovery_slot == node.object.slot()
426                && owner_grant == &node.owner_grant =>
427            {
428                Ok(Some(OwnerRecoveryCursor {
429                    owner_grant: owner_grant.clone(),
430                    position: OwnerRecoveryPosition::At { node: node.clone() },
431                }))
432            }
433            (
434                StoreDeviceRegistrationOrigin::Join { attempt_id, .. },
435                StoreDeviceRegistrationActivation::Join {
436                    attempt_id: activated_attempt_id,
437                },
438            ) if attempt_id == activated_attempt_id => Ok(None),
439            (
440                StoreDeviceRegistrationOrigin::Founder { .. },
441                StoreDeviceRegistrationActivation::Founder { .. },
442            ) => Ok(None),
443            _ => Err(StoreProtocolError::Malformed(
444                "registration origin differs from its exact activation authority".to_string(),
445            )),
446        }
447    }
448}
449
450impl<'de> Deserialize<'de> for ActivatedStoreDeviceRegistration {
451    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
452    where
453        D: serde::Deserializer<'de>,
454    {
455        #[derive(Deserialize)]
456        #[serde(deny_unknown_fields)]
457        struct EncodedActivation {
458            registration: ReferencedStoreDeviceRegistration,
459            activation: StoreDeviceRegistrationActivation,
460        }
461
462        let encoded = EncodedActivation::deserialize(deserializer)?;
463        Self::verified(encoded.registration, encoded.activation).map_err(serde::de::Error::custom)
464    }
465}
466
467impl StoreDeviceRegistration {
468    fn device_stream_activation(
469        &self,
470        reference: &StoreDeviceRegistrationRef,
471        anchor: &DeviceStreamAnchor,
472    ) -> Result<StreamActivation, StoreProtocolError> {
473        reference.verify_registration(self)?;
474        Ok(StreamActivation::device_authorized(
475            self.store_root.store_root_hash,
476            reference.clone(),
477            anchor.clone(),
478        ))
479    }
480
481    pub fn store_announcement_activation(
482        &self,
483        reference: &StoreDeviceRegistrationRef,
484    ) -> Result<StreamActivation, StoreProtocolError> {
485        self.device_stream_activation(reference, &self.store_commits)
486    }
487
488    pub fn store_acknowledgement_activation(
489        &self,
490        reference: &StoreDeviceRegistrationRef,
491    ) -> Result<StreamActivation, StoreProtocolError> {
492        self.device_stream_activation(reference, &self.acknowledgements)
493    }
494
495    pub fn store_snapshot_activation(
496        &self,
497        reference: &StoreDeviceRegistrationRef,
498    ) -> Result<StreamActivation, StoreProtocolError> {
499        self.device_stream_activation(reference, &self.snapshots)
500    }
501
502    pub fn signed(
503        store_root: StoreRootRef,
504        origin: StoreDeviceRegistrationOrigin,
505        provider: ProviderDeviceBinding,
506        store_commits: DeviceStreamAnchor,
507        acknowledgements: DeviceStreamAnchor,
508        snapshots: DeviceStreamAnchor,
509        identity_signer: &UserKeypair,
510    ) -> Result<Self, StoreProtocolError> {
511        validate_registration_anchors(&store_commits, &acknowledgements, &snapshots)?;
512        let author_pubkey = keys::public_key_hex(identity_signer);
513        let device_signer = derive_device_signer(identity_signer, &store_root, &origin);
514        let device_signing_pubkey = keys::public_key_hex(&device_signer);
515        let device_id = StoreDeviceId::derive(&store_root, &origin);
516        Ok(Signed::sign(
517            StoreDeviceRegistrationBody {
518                store_root,
519                device_id,
520                author_pubkey,
521                device_signing_pubkey,
522                origin,
523                provider,
524                store_commits,
525                acknowledgements,
526                snapshots,
527            },
528            identity_signer,
529        ))
530    }
531
532    pub fn device_signer(
533        &self,
534        identity_signer: &UserKeypair,
535    ) -> Result<UserKeypair, StoreProtocolError> {
536        if keys::public_key_hex(identity_signer) != self.author_pubkey {
537            return Err(StoreProtocolError::InvalidSignature);
538        }
539        let signer = derive_device_signer(identity_signer, &self.store_root, &self.origin);
540        if keys::public_key_hex(&signer) != self.device_signing_pubkey {
541            return Err(StoreProtocolError::InvalidSignature);
542        }
543        Ok(signer)
544    }
545
546    pub fn registration_hash(&self) -> ObjectHash {
547        self.hash()
548    }
549
550    pub fn parse_at(
551        bytes: &[u8],
552        expected_store_root: &StoreRootRef,
553        expected_device: StoreDeviceId,
554    ) -> Result<Self, StoreProtocolError> {
555        let registration: Self = crate::objects::decode_protocol_object(bytes)?;
556        registration.require_version()?;
557        if &registration.store_root != expected_store_root {
558            return Err(StoreProtocolError::StoreRootMismatch {
559                expected: expected_store_root.store_root_hash,
560                actual: registration.store_root.store_root_hash,
561            });
562        }
563        if registration.device_id != expected_device {
564            return Err(StoreProtocolError::RelocatedSlot {
565                expected: registration_slot_prefix(&expected_device.to_string()),
566                actual: registration_slot_prefix(&registration.device_id.to_string()),
567            });
568        }
569        if registration.device_id
570            != StoreDeviceId::derive(&registration.store_root, &registration.origin)
571        {
572            return Err(StoreProtocolError::Malformed(
573                "Store device id differs from its root and origin".to_string(),
574            ));
575        }
576        validate_registration_anchors(
577            &registration.store_commits,
578            &registration.acknowledgements,
579            &registration.snapshots,
580        )?;
581        let author_pubkey = registration.author_pubkey.clone();
582        registration.verify_by(&author_pubkey)?;
583        Ok(registration)
584    }
585}
586
587fn derive_device_signer(
588    identity_signer: &UserKeypair,
589    store_root: &StoreRootRef,
590    origin: &StoreDeviceRegistrationOrigin,
591) -> UserKeypair {
592    const DOMAIN: &[u8] = b"coven.store-device-signing-key.v1\0";
593    let context = serde_json::to_vec(&(store_root, origin))
594        .expect("Store device signing context serialization cannot fail");
595    identity_signer.derive_signing_key(DOMAIN, &context)
596}
597
598fn validate_registration_anchors(
599    commits: &DeviceStreamAnchor,
600    acknowledgements: &DeviceStreamAnchor,
601    snapshots: &DeviceStreamAnchor,
602) -> Result<(), StoreProtocolError> {
603    if !matches!(
604        acknowledgements,
605        DeviceStreamAnchor::StoreAcknowledgements { .. }
606    ) || !matches!(snapshots, DeviceStreamAnchor::StoreSnapshots { .. })
607        || !matches!(commits, DeviceStreamAnchor::StoreAnnouncements { .. })
608    {
609        return Err(StoreProtocolError::Malformed(
610            "Store device registration contains mismatched permanent stream anchors".to_string(),
611        ));
612    }
613    Ok(())
614}