Skip to main content

coven_protocol/store_commit/
device_join.rs

1use super::*;
2
3const STORE_DEVICE_ID_DOMAIN: &[u8] = b"coven.store-device-id.v1\0";
4
5/// The stable identity of one device in a Store, derived from the Store root and
6/// the device's registration origin. It names a device across the protocol — in
7/// membership, commit authorship, and epoch-close participation — and is what
8/// `Circles::exclude_close_device` and `Circles::close_status` address.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10#[serde(transparent)]
11pub struct StoreDeviceId(ObjectHash);
12
13impl StoreDeviceId {
14    pub fn derive(store_root: &StoreRootRef, origin: &StoreDeviceRegistrationOrigin) -> Self {
15        let mut material = STORE_DEVICE_ID_DOMAIN.to_vec();
16        material.extend(
17            serde_json::to_vec(&(store_root, origin.external_id()))
18                .expect("Store device identity serialization cannot fail"),
19        );
20        Self(ObjectHash::digest(&material))
21    }
22}
23
24impl fmt::Display for StoreDeviceId {
25    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26        fmt::Display::fmt(&self.0, formatter)
27    }
28}
29
30impl FromStr for StoreDeviceId {
31    type Err = StoreProtocolError;
32
33    fn from_str(value: &str) -> Result<Self, Self::Err> {
34        Ok(Self(value.parse()?))
35    }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
39#[serde(transparent)]
40pub struct StoreCreationId(ObjectHash);
41
42impl StoreCreationId {
43    pub fn from_random_bytes(bytes: [u8; 32]) -> Self {
44        Self(ObjectHash::from_digest(bytes))
45    }
46
47    pub(super) fn object_hash(self) -> ObjectHash {
48        self.0
49    }
50
51    #[cfg(any(test, feature = "test-utils"))]
52    #[cfg(any(test, feature = "test-utils"))]
53    pub fn from_nonce(nonce: &str) -> Self {
54        Self(ObjectHash::digest(nonce.as_bytes()))
55    }
56}
57
58impl fmt::Display for StoreCreationId {
59    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60        fmt::Display::fmt(&self.0, formatter)
61    }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
65#[serde(transparent)]
66pub struct DeviceJoinAttemptId(ObjectHash);
67
68impl DeviceJoinAttemptId {
69    pub fn from_hash(hash: ObjectHash) -> Self {
70        Self(hash)
71    }
72
73    pub(super) fn object_hash(self) -> ObjectHash {
74        self.0
75    }
76}
77
78impl fmt::Display for DeviceJoinAttemptId {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        fmt::Display::fmt(&self.0, formatter)
81    }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
85#[serde(transparent)]
86pub struct DeviceRecoveryId(ObjectHash);
87
88impl DeviceRecoveryId {
89    pub fn from_hash(hash: ObjectHash) -> Self {
90        Self(hash)
91    }
92
93    pub(super) fn object_hash(self) -> ObjectHash {
94        self.0
95    }
96}
97
98impl fmt::Display for DeviceRecoveryId {
99    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
100        fmt::Display::fmt(&self.0, formatter)
101    }
102}
103
104/// The wire body of a joining device's readiness proof. Every field here is
105/// signed.
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(deny_unknown_fields)]
108pub struct DeviceReadinessProofBody {
109    pub store_root_hash: ObjectHash,
110    pub attempt_id: DeviceJoinAttemptId,
111    pub registration: StoreDeviceRegistrationRef,
112    pub initial_ack: StoreAckRef,
113    pub bootstrap_cut: StoreHistoryCut,
114}
115
116impl SignedBody for DeviceReadinessProofBody {
117    const DOMAIN: &'static [u8] = DEVICE_READINESS_DOMAIN;
118}
119
120pub type DeviceReadinessProof = Signed<DeviceReadinessProofBody>;
121
122impl DeviceReadinessProof {
123    pub fn signed(
124        attempt_id: DeviceJoinAttemptId,
125        registration: StoreDeviceRegistrationRef,
126        initial_ack: StoreAckRef,
127        bootstrap_cut: StoreHistoryCut,
128        registration_value: &StoreDeviceRegistration,
129        device_signer: &UserKeypair,
130    ) -> Result<Self, StoreProtocolError> {
131        registration.verify_registration(registration_value)?;
132        if keys::public_key_hex(device_signer) != registration_value.device_signing_pubkey {
133            return Err(StoreProtocolError::InvalidSignature);
134        }
135        let body = DeviceReadinessProofBody {
136            store_root_hash: registration_value.store_root.store_root_hash,
137            attempt_id,
138            registration,
139            initial_ack,
140            bootstrap_cut,
141        };
142        validate_store_history_cut(&body.bootstrap_cut)?;
143        Ok(Signed::sign(body, device_signer))
144    }
145
146    /// Check a readiness proof against the attempt commit it answers.
147    ///
148    /// `attempt_cut` is that commit's predecessor cut — the history the
149    /// admitting device declared the joining device would install from. The
150    /// joiner echoes it here, so the two have to agree.
151    pub fn verify(
152        &self,
153        attempt_id: DeviceJoinAttemptId,
154        attempt_cut: &StoreHistoryCut,
155        registration: &StoreDeviceRegistration,
156        initial_ack_ref: &StoreAckRef,
157        initial_ack: &StoreAck,
158    ) -> Result<(), StoreProtocolError> {
159        if self.attempt_id != attempt_id
160            || self.store_root_hash != registration.store_root.store_root_hash
161            || self.registration.device_id != registration.device_id
162            || &self.bootstrap_cut != attempt_cut
163        {
164            return Err(StoreProtocolError::DeviceReadinessMismatch);
165        }
166        self.registration.verify_registration(registration)?;
167        if initial_ack.registration != self.registration
168            || initial_ack.sequence != 1
169            || initial_ack.successor.predecessor.is_some()
170            || initial_ack_ref != &self.initial_ack
171            || initial_ack_ref.registration != self.registration
172            || initial_ack_ref.sequence != initial_ack.sequence
173            || initial_ack_ref.ack_hash != initial_ack.ack_hash()
174            || initial_ack.store_cut != self.bootstrap_cut
175        {
176            return Err(StoreProtocolError::DeviceReadinessMismatch);
177        }
178        validate_store_history_cut(&self.bootstrap_cut)?;
179        self.verify_by(&registration.device_signing_pubkey)
180    }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
184#[serde(deny_unknown_fields)]
185pub struct OwnerRecoveryNodeRef {
186    pub owner_pubkey: String,
187    pub owner_grant: MembershipGrantId,
188    pub sequence: u64,
189    pub node_hash: ObjectHash,
190    pub object: ExactObjectRef,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
194#[serde(transparent)]
195pub struct OwnerRecoveryActivationId(ObjectHash);
196
197impl OwnerRecoveryActivationId {
198    pub fn derive(
199        root: &StoreRootRef,
200        owner_pubkey: &str,
201        owner_grant: &MembershipGrantId,
202        anchor: &GrantStreamAnchor,
203    ) -> Result<Self, StoreProtocolError> {
204        if !matches!(anchor, GrantStreamAnchor::OwnerRecovery { .. }) {
205            return Err(StoreProtocolError::OwnerRecoveryMismatch);
206        }
207        Ok(Self(ObjectHash::digest(&domain_json(
208            b"coven.owner-recovery-activation.v1\0",
209            &(root, owner_pubkey, owner_grant, anchor),
210        ))))
211    }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
215#[serde(rename_all = "snake_case", deny_unknown_fields)]
216pub enum OwnerRecoveryPosition {
217    BeforeFirst {
218        activation: OwnerRecoveryActivationId,
219    },
220    At {
221        node: OwnerRecoveryNodeRef,
222    },
223}
224
225impl OwnerRecoveryPosition {
226    /// The position two predecessor states of one Owner grant agree on. The
227    /// grant's recovery nodes form one chain in exact slots, one node per
228    /// sequence, so two positions on it are ordered: a node is past the
229    /// activation it follows, and a higher sequence is past a lower one. Two
230    /// states that name different nodes at one sequence, or different
231    /// activations before the first node, are not on one chain.
232    pub fn merge(&self, other: &Self) -> Result<Self, super::StoreProtocolError> {
233        match (self, other) {
234            (Self::BeforeFirst { activation }, Self::BeforeFirst { activation: other })
235                if activation == other =>
236            {
237                Ok(self.clone())
238            }
239            (Self::BeforeFirst { .. }, Self::BeforeFirst { .. }) => {
240                Err(super::StoreProtocolError::OwnerRecoveryMismatch)
241            }
242            (Self::BeforeFirst { .. }, Self::At { .. }) => Ok(other.clone()),
243            (Self::At { .. }, Self::BeforeFirst { .. }) => Ok(self.clone()),
244            (Self::At { node }, Self::At { node: other_node }) => {
245                match node.sequence.cmp(&other_node.sequence) {
246                    std::cmp::Ordering::Less => Ok(other.clone()),
247                    std::cmp::Ordering::Greater => Ok(self.clone()),
248                    std::cmp::Ordering::Equal if node == other_node => Ok(self.clone()),
249                    std::cmp::Ordering::Equal => {
250                        Err(super::StoreProtocolError::OwnerRecoveryMismatch)
251                    }
252                }
253            }
254        }
255    }
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
259#[serde(deny_unknown_fields)]
260pub struct OwnerRecoveryCursor {
261    pub owner_grant: MembershipGrantId,
262    pub position: OwnerRecoveryPosition,
263}
264
265#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
266#[serde(deny_unknown_fields)]
267pub struct DeviceJoinAbandonmentRef {
268    pub attempt_id: DeviceJoinAttemptId,
269    pub abandonment_hash: ObjectHash,
270    pub object: ExactObjectRef,
271}