Skip to main content

coven_protocol/store_commit/
device_join_journal.rs

1//! The durable device-join journal model: role progress, and the status and
2//! action each recorded step derives.
3
4use serde::{Deserialize, Serialize};
5
6use crate::objects::ExactObjectRef;
7use crate::provider::StoreMemberProviderAccessGrant;
8use crate::store_commit::device_join_exchange::{
9    DeviceJoinAbandonment, DeviceJoinActivation, DeviceJoinOffer, DeviceJoinReadiness,
10    DeviceProviderAccessRequest, DeviceProviderAdmissionApproval,
11    DeviceProviderAdmissionCompletion, DeviceRegistrationRequest, ProviderReadyDeviceBootstrap,
12    ProvisionalDeviceBootstrap, SamePrincipalDeviceJoin,
13};
14
15use super::*;
16use crate::store_commit::DeviceJoinAbandonmentRef;
17
18/// Derived from a journal record on demand and never stored, so it carries no
19/// wire form of its own.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum DeviceJoinStatus {
22    AwaitingAccessRequest {
23        offer: DeviceJoinOffer,
24    },
25    AwaitingProviderAdmission {
26        request: DeviceProviderAccessRequest,
27    },
28    AwaitingRegistrationRequest {
29        approval: DeviceProviderAdmissionApproval,
30    },
31    AwaitingBootstrap {
32        request: DeviceRegistrationRequest,
33    },
34    SamePrincipalActivationCreatePending {
35        request: DeviceRegistrationRequest,
36    },
37    AwaitingChallengePublication {
38        bootstrap: ProvisionalDeviceBootstrap,
39    },
40    AwaitingReadiness {
41        bootstrap: ProviderReadyDeviceBootstrap,
42    },
43    AwaitingProviderCompletion {
44        readiness: DeviceJoinReadiness,
45    },
46    AwaitingActivation {
47        completion: DeviceProviderAdmissionCompletion,
48    },
49    AwaitingCompletion {
50        activation: DeviceJoinActivation,
51    },
52    SamePrincipalCompleted {
53        join: SamePrincipalDeviceJoin,
54    },
55    Abandoned {
56        abandonment: DeviceJoinAbandonment,
57    },
58    ProviderAccessGrantCreatePending {
59        request: DeviceProviderAccessRequest,
60        grant: StoreMemberProviderAccessGrant,
61    },
62    AbandonmentCreatePending {
63        abandonment: DeviceJoinAbandonmentRef,
64    },
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case", deny_unknown_fields)]
69pub enum DeviceJoinAction {
70    TransferOffer(DeviceJoinOffer),
71    TransferProviderAccessRequest(DeviceProviderAccessRequest),
72    TransferProviderAdmissionApproval(DeviceProviderAdmissionApproval),
73    TransferRegistrationRequest(DeviceRegistrationRequest),
74    TransferProviderReadyBootstrap(ProviderReadyDeviceBootstrap),
75    TransferReadiness(DeviceJoinReadiness),
76    TransferSamePrincipalJoin(SamePrincipalDeviceJoin),
77    TransferActivation(DeviceJoinActivation),
78    TransferAbandonment(DeviceJoinAbandonment),
79    CompleteJoin(DeviceJoinActivation),
80    ResumeOperation {
81        attempt_id: DeviceJoinAttemptId,
82        role: DeviceJoinRole,
83    },
84}
85
86#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case", deny_unknown_fields)]
88pub enum OwnerJoinProgress {
89    Offered(DeviceJoinOffer),
90    /// The joining device asked for provider access. The admitting device holds
91    /// the store's provider-administrator grant, so answering this request, the
92    /// grant it prepares, and the approval it signs are all its own steps.
93    AccessRequested(DeviceProviderAccessRequest),
94    AccessGrantPrepared {
95        request: DeviceProviderAccessRequest,
96        grant: StoreMemberProviderAccessGrant,
97        prepared: PreparedDeviceJoinObject,
98    },
99    ApprovalPrepared(DeviceProviderAdmissionApproval),
100    RegistrationRequested(DeviceRegistrationRequest),
101    AbandonmentCreateIntent {
102        offer: DeviceJoinOffer,
103        abandonment: DeviceJoinAbandonmentRef,
104        prepared: PreparedDeviceJoinObject,
105    },
106    AttemptActivated(ProvisionalDeviceBootstrap),
107    ChallengeCreateIntent(ProvisionalDeviceBootstrap),
108    ProviderReady(ProviderReadyDeviceBootstrap),
109    ResponseObserved(DeviceJoinReadiness),
110    Completed(DeviceProviderAdmissionCompletion),
111    SamePrincipalActivationCreateIntent {
112        request: DeviceRegistrationRequest,
113        bootstrap_cut: StoreHistoryCut,
114        membership: StoreMembershipStateRef,
115        registration: StoreDeviceRegistrationRef,
116        registration_prepared: PreparedDeviceJoinObject,
117    },
118    ActivationCreateIntent {
119        completion: DeviceProviderAdmissionCompletion,
120    },
121    /// The owner published the activation commit and has nothing left to do
122    /// but hand the artifact over.
123    ///
124    /// `registration` is the joined device's, carried so the owner can tell
125    /// when that device has actually arrived: its announcement stream id is a
126    /// pure function of this reference, and a stream that appears in the
127    /// materialized frontier is the device's own first commit — the one thing
128    /// it publishes that the owner did not write for it.
129    ActivationPrepared {
130        completion: DeviceProviderAdmissionCompletion,
131        activation: DeviceJoinActivation,
132        registration: StoreDeviceRegistrationRef,
133    },
134    /// The same-principal join completed, carried closure and all.
135    ///
136    /// `registration` is the joined device's, for the same reason
137    /// [`ActivationPrepared`](Self::ActivationPrepared) carries one: this row is
138    /// the largest a join writes — a snapshot's metadata and the bootstrap
139    /// closure live inside `join` — and the owner needs to be able to tell when
140    /// the device it activated has arrived so the row can go.
141    SamePrincipalCompleted {
142        join: SamePrincipalDeviceJoin,
143        registration: StoreDeviceRegistrationRef,
144    },
145    Abandoned(DeviceJoinAbandonment),
146}
147
148#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(deny_unknown_fields)]
150pub struct PreparedDeviceJoinObject {
151    pub object: ExactObjectRef,
152    pub stored_bytes: Vec<u8>,
153}
154
155impl PreparedDeviceJoinObject {
156    pub fn from_prepared(prepared: &crate::objects::PreparedExactObject) -> Self {
157        Self {
158            object: prepared.reference().clone(),
159            stored_bytes: prepared.stored_bytes().to_vec(),
160        }
161    }
162
163    /// The prepared object this journal entry recorded, ready to be created
164    /// again. A resumed write creates these exact bytes rather than preparing a
165    /// second object, so the retry writes what the journal already committed to.
166    pub fn restore(
167        &self,
168    ) -> Result<crate::objects::PreparedExactObject, crate::objects::StorageError> {
169        crate::objects::PreparedExactObject::new(self.object.clone(), self.stored_bytes.clone())
170    }
171}
172
173#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(rename_all = "snake_case", deny_unknown_fields)]
175pub enum JoinerJoinProgress {
176    OfferReceived(DeviceJoinOffer),
177    AccessRequested(DeviceProviderAccessRequest),
178    ApprovalReceived(DeviceProviderAdmissionApproval),
179    RegistrationPrepared(DeviceRegistrationRequest),
180    Ready(DeviceJoinReadiness),
181    ActivationObserved {
182        readiness: DeviceJoinReadiness,
183        activation: DeviceJoinActivation,
184    },
185}
186
187#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "snake_case", deny_unknown_fields)]
189pub enum DeviceJoinRoleProgress {
190    Owner(OwnerJoinProgress),
191    Joiner(JoinerJoinProgress),
192}
193
194/// A role's own progress type. Each one names the role whose journal rows hold
195/// it, so a journal bound to a role accepts only that role's progress.
196pub trait DeviceJoinRoleProgressKind: Into<DeviceJoinRoleProgress> {
197    const ROLE: DeviceJoinRole;
198}
199
200impl From<OwnerJoinProgress> for DeviceJoinRoleProgress {
201    fn from(progress: OwnerJoinProgress) -> Self {
202        Self::Owner(progress)
203    }
204}
205
206impl DeviceJoinRoleProgressKind for OwnerJoinProgress {
207    const ROLE: DeviceJoinRole = DeviceJoinRole::Owner;
208}
209
210impl From<JoinerJoinProgress> for DeviceJoinRoleProgress {
211    fn from(progress: JoinerJoinProgress) -> Self {
212        Self::Joiner(progress)
213    }
214}
215
216impl DeviceJoinRoleProgressKind for JoinerJoinProgress {
217    const ROLE: DeviceJoinRole = DeviceJoinRole::Joiner;
218}
219
220impl DeviceJoinRoleProgress {
221    pub fn role(&self) -> DeviceJoinRole {
222        match self {
223            Self::Owner(_) => DeviceJoinRole::Owner,
224            Self::Joiner(_) => DeviceJoinRole::Joiner,
225        }
226    }
227
228    pub fn role_name(&self) -> &'static str {
229        self.role().as_str()
230    }
231}
232
233#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
234#[serde(deny_unknown_fields)]
235pub struct DeviceJoinJournalRecord {
236    pub attempt_id: DeviceJoinAttemptId,
237    pub progress: Box<DeviceJoinRoleProgress>,
238}
239
240impl DeviceJoinJournalRecord {
241    pub fn owner_offered(offer: DeviceJoinOffer) -> Self {
242        Self {
243            attempt_id: offer.attempt_id,
244            progress: Box::new(DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(
245                offer,
246            ))),
247        }
248    }
249
250    pub fn store_key(&self) -> String {
251        store_journal_key(self.attempt_id, self.progress.role_name())
252    }
253
254    pub fn store_key_for(attempt_id: DeviceJoinAttemptId, role: DeviceJoinRole) -> String {
255        store_journal_key(attempt_id, role.as_str())
256    }
257
258    pub fn status(&self) -> DeviceJoinStatus {
259        device_join_status(self)
260    }
261
262    pub fn action(&self) -> Option<DeviceJoinAction> {
263        device_join_action(self)
264    }
265
266    pub fn sort_key(&self) -> (DeviceJoinAttemptId, DeviceJoinRole) {
267        (self.attempt_id, self.progress.role())
268    }
269
270    pub fn attempt_key(&self) -> String {
271        attempt_key(self.attempt_id)
272    }
273}
274
275#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
276#[serde(rename_all = "snake_case", deny_unknown_fields)]
277/// The two sides of a join. One device admits — it answers the access
278/// request, prepares the storage grant, signs the approval, registers the
279/// device and activates it — and the other is the device being admitted.
280pub enum DeviceJoinRole {
281    Owner,
282    Joiner,
283}
284
285impl DeviceJoinRole {
286    pub fn as_str(self) -> &'static str {
287        match self {
288            Self::Owner => "owner",
289            Self::Joiner => "joiner",
290        }
291    }
292}
293
294pub(crate) fn device_join_status(record: &DeviceJoinJournalRecord) -> DeviceJoinStatus {
295    match &*record.progress {
296        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(offer))
297        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::OfferReceived(offer)) => {
298            DeviceJoinStatus::AwaitingAccessRequest {
299                offer: offer.clone(),
300            }
301        }
302        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AccessRequested(request))
303        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::AccessRequested(request)) => {
304            DeviceJoinStatus::AwaitingProviderAdmission {
305                request: request.clone(),
306            }
307        }
308        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AccessGrantPrepared {
309            request,
310            grant,
311            ..
312        }) => DeviceJoinStatus::ProviderAccessGrantCreatePending {
313            request: request.clone(),
314            grant: grant.clone(),
315        },
316        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ApprovalPrepared(approval))
317        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ApprovalReceived(approval)) => {
318            DeviceJoinStatus::AwaitingRegistrationRequest {
319                approval: approval.clone(),
320            }
321        }
322        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::RegistrationRequested(request))
323        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::RegistrationPrepared(request)) => {
324            DeviceJoinStatus::AwaitingBootstrap {
325                request: request.clone(),
326            }
327        }
328        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::SamePrincipalActivationCreateIntent {
329            request,
330            ..
331        }) => DeviceJoinStatus::SamePrincipalActivationCreatePending {
332            request: request.clone(),
333        },
334        DeviceJoinRoleProgress::Owner(
335            OwnerJoinProgress::AttemptActivated(bootstrap)
336            | OwnerJoinProgress::ChallengeCreateIntent(bootstrap),
337        ) => DeviceJoinStatus::AwaitingChallengePublication {
338            bootstrap: bootstrap.clone(),
339        },
340        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(bootstrap)) => {
341            DeviceJoinStatus::AwaitingReadiness {
342                bootstrap: bootstrap.clone(),
343            }
344        }
345        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ResponseObserved(readiness))
346        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(readiness)) => {
347            DeviceJoinStatus::AwaitingProviderCompletion {
348                readiness: readiness.clone(),
349            }
350        }
351        DeviceJoinRoleProgress::Owner(
352            OwnerJoinProgress::ActivationCreateIntent { completion, .. }
353            | OwnerJoinProgress::Completed(completion),
354        ) => DeviceJoinStatus::AwaitingActivation {
355            completion: completion.clone(),
356        },
357        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::SamePrincipalCompleted {
358            join, ..
359        }) => DeviceJoinStatus::SamePrincipalCompleted { join: join.clone() },
360        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ActivationPrepared {
361            activation, ..
362        })
363        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ActivationObserved {
364            activation,
365            ..
366        }) => DeviceJoinStatus::AwaitingCompletion {
367            activation: activation.clone(),
368        },
369        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Abandoned(abandonment)) => {
370            DeviceJoinStatus::Abandoned {
371                abandonment: abandonment.clone(),
372            }
373        }
374        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AbandonmentCreateIntent {
375            abandonment,
376            ..
377        }) => DeviceJoinStatus::AbandonmentCreatePending {
378            abandonment: abandonment.clone(),
379        },
380    }
381}
382
383pub fn device_join_action(record: &DeviceJoinJournalRecord) -> Option<DeviceJoinAction> {
384    let resume = || DeviceJoinAction::ResumeOperation {
385        attempt_id: record.attempt_id,
386        role: record.progress.role(),
387    };
388    match &*record.progress {
389        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(offer)) => {
390            Some(DeviceJoinAction::TransferOffer(offer.clone()))
391        }
392        DeviceJoinRoleProgress::Owner(
393            OwnerJoinProgress::AccessRequested(_)
394            | OwnerJoinProgress::AccessGrantPrepared { .. }
395            | OwnerJoinProgress::RegistrationRequested(_)
396            | OwnerJoinProgress::AttemptActivated(_)
397            | OwnerJoinProgress::ChallengeCreateIntent(_)
398            | OwnerJoinProgress::ResponseObserved(_)
399            | OwnerJoinProgress::Completed(_)
400            | OwnerJoinProgress::SamePrincipalActivationCreateIntent { .. }
401            | OwnerJoinProgress::AbandonmentCreateIntent { .. }
402            | OwnerJoinProgress::ActivationCreateIntent { .. },
403        ) => Some(resume()),
404        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ApprovalPrepared(approval)) => Some(
405            DeviceJoinAction::TransferProviderAdmissionApproval(approval.clone()),
406        ),
407        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(bootstrap)) => Some(
408            DeviceJoinAction::TransferProviderReadyBootstrap(bootstrap.clone()),
409        ),
410        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::SamePrincipalCompleted {
411            join, ..
412        }) => Some(DeviceJoinAction::TransferSamePrincipalJoin(join.clone())),
413        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ActivationPrepared {
414            activation, ..
415        }) => Some(DeviceJoinAction::TransferActivation(activation.clone())),
416        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Abandoned(abandonment)) => {
417            Some(DeviceJoinAction::TransferAbandonment(abandonment.clone()))
418        }
419
420        DeviceJoinRoleProgress::Joiner(
421            JoinerJoinProgress::OfferReceived(_) | JoinerJoinProgress::ApprovalReceived(_),
422        ) => Some(resume()),
423        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::AccessRequested(request)) => Some(
424            DeviceJoinAction::TransferProviderAccessRequest(request.clone()),
425        ),
426        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::RegistrationPrepared(request)) => Some(
427            DeviceJoinAction::TransferRegistrationRequest(request.clone()),
428        ),
429        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(readiness)) => {
430            Some(DeviceJoinAction::TransferReadiness(readiness.clone()))
431        }
432        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ActivationObserved {
433            activation,
434            ..
435        }) => Some(DeviceJoinAction::CompleteJoin(activation.clone())),
436    }
437}
438
439pub(crate) fn store_journal_key(attempt_id: DeviceJoinAttemptId, role: &str) -> String {
440    format!("device_join/{}/{role}", attempt_key(attempt_id))
441}
442
443pub fn attempt_key(attempt_id: DeviceJoinAttemptId) -> String {
444    serde_json::to_value(attempt_id)
445        .expect("device join attempt id serialization cannot fail")
446        .as_str()
447        .expect("device join attempt id serializes as a string")
448        .to_string()
449}