Skip to main content

coven_protocol/store_commit/device_state/
exclusion.rs

1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
4#[serde(transparent)]
5pub struct StoreDeviceExclusionProposalId(ObjectHash);
6
7impl StoreDeviceExclusionProposalId {
8    pub fn from_hash(hash: ObjectHash) -> Self {
9        Self(hash)
10    }
11}
12
13impl fmt::Display for StoreDeviceExclusionProposalId {
14    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
15        fmt::Display::fmt(&self.0, formatter)
16    }
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct StoreDeviceExclusionProposalRef {
22    pub proposal_id: StoreDeviceExclusionProposalId,
23    pub target: StoreDeviceRegistrationRef,
24    pub proposal_hash: ObjectHash,
25    pub object: ExactObjectRef,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
29#[serde(deny_unknown_fields)]
30pub struct StoreDeviceExclusionRef {
31    pub proposal: StoreDeviceExclusionProposalRef,
32    pub outcome_hash: ObjectHash,
33    pub object: ExactObjectRef,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct StoreDeviceExclusionCancellationRef {
39    pub proposal: StoreDeviceExclusionProposalRef,
40    pub outcome_hash: ObjectHash,
41    pub object: ExactObjectRef,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case", deny_unknown_fields)]
46pub enum StoreDeviceExclusionOutcomeRef {
47    Excluded(StoreDeviceExclusionRef),
48    Cancelled(StoreDeviceExclusionCancellationRef),
49}
50
51#[derive(Debug)]
52pub struct VerifiedDeviceExclusionProposal {
53    pub reference: StoreDeviceExclusionProposalRef,
54    pub object: crate::objects::VerifiedObject<StoreDeviceExclusionProposal>,
55    pub target: StoreDeviceRegistration,
56    pub owner: StoreDeviceRegistration,
57}
58
59#[derive(Debug)]
60pub struct VerifiedDeviceExclusionOutcome {
61    pub object: crate::objects::VerifiedObject<StoreDeviceExclusionOutcome>,
62    pub owner: StoreDeviceRegistration,
63}
64
65impl StoreDeviceExclusionOutcomeRef {
66    pub fn proposal(&self) -> &StoreDeviceExclusionProposalRef {
67        match self {
68            Self::Excluded(reference) => &reference.proposal,
69            Self::Cancelled(reference) => &reference.proposal,
70        }
71    }
72
73    pub fn object(&self) -> &ExactObjectRef {
74        match self {
75            Self::Excluded(reference) => &reference.object,
76            Self::Cancelled(reference) => &reference.object,
77        }
78    }
79
80    pub fn from_outcome(
81        outcome: &StoreDeviceExclusionOutcome,
82        proposal: &StoreDeviceExclusionProposal,
83        object: ExactObjectRef,
84    ) -> Result<Self, StoreProtocolError> {
85        if object.slot() != &proposal.outcome_slot
86            || outcome.proposal().proposal_id != proposal.proposal_id
87        {
88            return Err(StoreProtocolError::DeviceStateMismatch);
89        }
90        Ok(match outcome {
91            StoreDeviceExclusionOutcome::Excluded(exclusion) => {
92                Self::Excluded(StoreDeviceExclusionRef {
93                    proposal: exclusion.proposal.clone(),
94                    outcome_hash: exclusion.outcome_hash(),
95                    object,
96                })
97            }
98            StoreDeviceExclusionOutcome::Cancelled(cancellation) => {
99                Self::Cancelled(StoreDeviceExclusionCancellationRef {
100                    proposal: cancellation.proposal.clone(),
101                    outcome_hash: cancellation.outcome_hash(),
102                    object,
103                })
104            }
105        })
106    }
107
108    pub fn outcome_hash(&self) -> ObjectHash {
109        match self {
110            Self::Excluded(reference) => reference.outcome_hash,
111            Self::Cancelled(reference) => reference.outcome_hash,
112        }
113    }
114}
115
116/// The wire body of a device-exclusion proposal. Every field here is signed.
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(deny_unknown_fields)]
119pub struct StoreDeviceExclusionProposalBody {
120    pub store_root_hash: ObjectHash,
121    pub proposal_id: StoreDeviceExclusionProposalId,
122    pub target: StoreDeviceRegistrationRef,
123    pub frozen_device_state: StoreDeviceStateRef,
124    pub outcome_slot: ObjectSlot,
125    pub owner_registration: StoreDeviceRegistrationRef,
126    pub owner_grant: MembershipGrantId,
127}
128
129impl SignedBody for StoreDeviceExclusionProposalBody {
130    const DOMAIN: &'static [u8] = DEVICE_EXCLUSION_PROPOSAL_DOMAIN;
131}
132
133pub type StoreDeviceExclusionProposal = Signed<StoreDeviceExclusionProposalBody>;
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case", deny_unknown_fields)]
137pub enum StoreDeviceExclusionOutcome {
138    Excluded(StoreDeviceExclusion),
139    Cancelled(StoreDeviceExclusionCancellation),
140}
141
142/// The wire body of an owner's withdrawal of an exclusion proposal. Every field
143/// here is signed.
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(deny_unknown_fields)]
146pub struct StoreDeviceExclusionCancellationBody {
147    pub store_root_hash: ObjectHash,
148    pub proposal: StoreDeviceExclusionProposalRef,
149    pub owner_registration: StoreDeviceRegistrationRef,
150    pub owner_grant: MembershipGrantId,
151}
152
153impl SignedBody for StoreDeviceExclusionCancellationBody {
154    const DOMAIN: &'static [u8] = DEVICE_EXCLUSION_CANCELLATION_DOMAIN;
155}
156
157pub type StoreDeviceExclusionCancellation = Signed<StoreDeviceExclusionCancellationBody>;
158
159/// The wire body of a device's exclusion. Every field here is signed.
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(deny_unknown_fields)]
162pub struct StoreDeviceExclusionBody {
163    pub store_root_hash: ObjectHash,
164    pub proposal: StoreDeviceExclusionProposalRef,
165    pub target: StoreDeviceRegistrationRef,
166    pub proof: StoreDeviceExclusionProof,
167    pub owner_registration: StoreDeviceRegistrationRef,
168    pub owner_grant: MembershipGrantId,
169}
170
171impl SignedBody for StoreDeviceExclusionBody {
172    const DOMAIN: &'static [u8] = DEVICE_EXCLUSION_DOMAIN;
173}
174
175pub type StoreDeviceExclusion = Signed<StoreDeviceExclusionBody>;
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(deny_unknown_fields)]
179pub struct StoreDeviceExclusionProof {
180    pub frozen_device_state: StoreDeviceStateRef,
181    pub remaining_device_acks: Vec<StoreAckRef>,
182    pub cutoff: StoreHistoryCut,
183}
184
185impl StoreDeviceExclusionProposal {
186    #[allow(clippy::too_many_arguments)]
187    pub fn signed(
188        store_root_hash: ObjectHash,
189        proposal_id: StoreDeviceExclusionProposalId,
190        target: StoreDeviceRegistrationRef,
191        target_registration: &StoreDeviceRegistration,
192        frozen_device_state: StoreDeviceStateRef,
193        outcome_slot: ObjectSlot,
194        owner_registration: StoreDeviceRegistrationRef,
195        owner_grant: MembershipGrantId,
196        owner: &StoreDeviceRegistration,
197        owner_device_signer: &UserKeypair,
198    ) -> Result<Self, StoreProtocolError> {
199        owner_registration.verify_registration(owner)?;
200        target.verify_registration(target_registration)?;
201        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey
202            || owner.store_root.store_root_hash != store_root_hash
203            || target_registration.store_root.store_root_hash != store_root_hash
204        {
205            return Err(StoreProtocolError::InvalidSignature);
206        }
207        let expected_outcome = format!(
208            "{}.json",
209            device_exclusion_outcome_semantic_prefix(target.device_id, proposal_id)
210        );
211        if outcome_slot.logical_key() != expected_outcome {
212            return Err(StoreProtocolError::RelocatedSlot {
213                expected: expected_outcome,
214                actual: outcome_slot.logical_key().to_string(),
215            });
216        }
217        validate_store_device_state_ref(&frozen_device_state)?;
218        Ok(Signed::sign(
219            StoreDeviceExclusionProposalBody {
220                store_root_hash,
221                proposal_id,
222                target,
223                frozen_device_state,
224                outcome_slot,
225                owner_registration,
226                owner_grant,
227            },
228            owner_device_signer,
229        ))
230    }
231
232    pub fn proposal_hash(&self) -> ObjectHash {
233        self.hash()
234    }
235
236    pub fn parse_at(
237        bytes: &[u8],
238        expected: &StoreDeviceExclusionProposalRef,
239        target: &StoreDeviceRegistration,
240        owner: &StoreDeviceRegistration,
241    ) -> Result<Self, StoreProtocolError> {
242        let proposal: Self = crate::objects::decode_protocol_object(bytes)?;
243        expected.verify_proposal(&proposal)?;
244        proposal.target.verify_registration(target)?;
245        proposal.owner_registration.verify_registration(owner)?;
246        validate_store_device_state_ref(&proposal.frozen_device_state)?;
247        let expected_outcome = format!(
248            "{}.json",
249            device_exclusion_outcome_semantic_prefix(
250                proposal.target.device_id,
251                proposal.proposal_id,
252            )
253        );
254        if proposal.outcome_slot.logical_key() != expected_outcome {
255            return Err(StoreProtocolError::RelocatedSlot {
256                expected: expected_outcome,
257                actual: proposal.outcome_slot.logical_key().to_string(),
258            });
259        }
260        if proposal.store_root_hash != owner.store_root.store_root_hash
261            || proposal.store_root_hash != target.store_root.store_root_hash
262        {
263            return Err(StoreProtocolError::InvalidSignature);
264        }
265        proposal.verify_by(&owner.device_signing_pubkey)?;
266        Ok(proposal)
267    }
268}
269
270impl StoreDeviceExclusionProposalRef {
271    pub fn from_proposal(
272        proposal: &StoreDeviceExclusionProposal,
273        object: ExactObjectRef,
274    ) -> Result<Self, StoreProtocolError> {
275        let reference = Self {
276            proposal_id: proposal.proposal_id,
277            target: proposal.target.clone(),
278            proposal_hash: proposal.proposal_hash(),
279            object,
280        };
281        reference.validate_path()?;
282        Ok(reference)
283    }
284
285    pub fn validate_path(&self) -> Result<(), StoreProtocolError> {
286        let expected = format!(
287            "{}.json",
288            device_exclusion_proposal_semantic_prefix(
289                self.target.device_id,
290                self.proposal_id,
291                self.proposal_hash,
292            )
293        );
294        if self.object.slot().logical_key() != expected {
295            return Err(StoreProtocolError::RelocatedSlot {
296                expected,
297                actual: self.object.slot().logical_key().to_string(),
298            });
299        }
300        Ok(())
301    }
302
303    pub fn verify_proposal(
304        &self,
305        proposal: &StoreDeviceExclusionProposal,
306    ) -> Result<(), StoreProtocolError> {
307        self.validate_path()?;
308        if self.proposal_id != proposal.proposal_id
309            || self.target != proposal.target
310            || self.proposal_hash != proposal.proposal_hash()
311        {
312            return Err(StoreProtocolError::DeviceStateMismatch);
313        }
314        Ok(())
315    }
316}
317
318impl StoreDeviceExclusionCancellation {
319    pub fn signed(
320        proposal: StoreDeviceExclusionProposalRef,
321        proposal_value: &StoreDeviceExclusionProposal,
322        owner_registration: StoreDeviceRegistrationRef,
323        owner_grant: MembershipGrantId,
324        owner: &StoreDeviceRegistration,
325        owner_device_signer: &UserKeypair,
326    ) -> Result<Self, StoreProtocolError> {
327        owner_registration.verify_registration(owner)?;
328        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey
329            || proposal.proposal_hash != proposal_value.proposal_hash()
330            || proposal.target != proposal_value.target
331            || proposal_value.store_root_hash != owner.store_root.store_root_hash
332        {
333            return Err(StoreProtocolError::InvalidSignature);
334        }
335        Ok(Signed::sign(
336            StoreDeviceExclusionCancellationBody {
337                store_root_hash: owner.store_root.store_root_hash,
338                proposal,
339                owner_registration,
340                owner_grant,
341            },
342            owner_device_signer,
343        ))
344    }
345
346    pub fn outcome_hash(&self) -> ObjectHash {
347        self.hash()
348    }
349}
350
351impl StoreDeviceExclusion {
352    #[allow(clippy::too_many_arguments)]
353    pub fn signed(
354        proposal: StoreDeviceExclusionProposalRef,
355        proposal_value: &StoreDeviceExclusionProposal,
356        target: StoreDeviceRegistrationRef,
357        target_registration: &StoreDeviceRegistration,
358        proof: StoreDeviceExclusionProof,
359        owner_registration: StoreDeviceRegistrationRef,
360        owner_grant: MembershipGrantId,
361        owner: &StoreDeviceRegistration,
362        owner_device_signer: &UserKeypair,
363    ) -> Result<Self, StoreProtocolError> {
364        owner_registration.verify_registration(owner)?;
365        target.verify_registration(target_registration)?;
366        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey
367            || proposal.target != target
368            || proposal.proposal_hash != proposal_value.proposal_hash()
369            || proposal.target != proposal_value.target
370            || target_registration.store_root.store_root_hash != owner.store_root.store_root_hash
371        {
372            return Err(StoreProtocolError::InvalidSignature);
373        }
374        validate_device_exclusion_proof(&proof)?;
375        Ok(Signed::sign(
376            StoreDeviceExclusionBody {
377                store_root_hash: owner.store_root.store_root_hash,
378                proposal,
379                target,
380                proof,
381                owner_registration,
382                owner_grant,
383            },
384            owner_device_signer,
385        ))
386    }
387
388    pub fn outcome_hash(&self) -> ObjectHash {
389        self.hash()
390    }
391}
392
393impl StoreDeviceExclusionOutcome {
394    pub fn outcome_hash(&self) -> ObjectHash {
395        match self {
396            Self::Excluded(exclusion) => exclusion.outcome_hash(),
397            Self::Cancelled(cancellation) => cancellation.outcome_hash(),
398        }
399    }
400
401    pub fn proposal(&self) -> &StoreDeviceExclusionProposalRef {
402        match self {
403            Self::Excluded(exclusion) => &exclusion.proposal,
404            Self::Cancelled(cancellation) => &cancellation.proposal,
405        }
406    }
407
408    pub fn to_bytes(&self) -> Vec<u8> {
409        serde_json::to_vec(self).expect("Store device exclusion outcome serialization cannot fail")
410    }
411
412    pub fn parse_at(
413        bytes: &[u8],
414        expected: &StoreDeviceExclusionOutcomeRef,
415        proposal: &StoreDeviceExclusionProposal,
416        target: &StoreDeviceRegistration,
417        owner: &StoreDeviceRegistration,
418    ) -> Result<Self, StoreProtocolError> {
419        let outcome: Self = crate::objects::decode_protocol_object(bytes)?;
420        if outcome.proposal().proposal_id != proposal.proposal_id
421            || outcome.proposal().proposal_hash != proposal.proposal_hash()
422            || outcome.proposal().target != proposal.target
423            || expected.proposal() != outcome.proposal()
424            || expected.object().slot() != &proposal.outcome_slot
425            || expected.outcome_hash() != outcome.outcome_hash()
426        {
427            return Err(StoreProtocolError::DeviceStateMismatch);
428        }
429        match &outcome {
430            Self::Excluded(exclusion) => {
431                exclusion.target.verify_registration(target)?;
432                exclusion.owner_registration.verify_registration(owner)?;
433                validate_device_exclusion_proof(&exclusion.proof)?;
434                if exclusion.store_root_hash != proposal.store_root_hash
435                    || exclusion.store_root_hash != target.store_root.store_root_hash
436                    || exclusion.target != proposal.target
437                {
438                    return Err(StoreProtocolError::InvalidSignature);
439                }
440                exclusion.verify_by(&owner.device_signing_pubkey)?;
441            }
442            Self::Cancelled(cancellation) => {
443                cancellation.owner_registration.verify_registration(owner)?;
444                if cancellation.store_root_hash != proposal.store_root_hash {
445                    return Err(StoreProtocolError::InvalidSignature);
446                }
447                cancellation.verify_by(&owner.device_signing_pubkey)?;
448            }
449        }
450        Ok(outcome)
451    }
452}
453
454fn validate_device_exclusion_proof(
455    proof: &StoreDeviceExclusionProof,
456) -> Result<(), StoreProtocolError> {
457    if proof
458        .remaining_device_acks
459        .windows(2)
460        .any(|pair| pair[0] >= pair[1])
461    {
462        return Err(StoreProtocolError::DeviceStateMismatch);
463    }
464    validate_store_device_state_ref(&proof.frozen_device_state)?;
465    validate_store_history_cut(&proof.cutoff)
466}