Skip to main content

coven_database/store/
device_join_journal.rs

1//! The adjacency rules the device-join journal's compare-and-swap update
2//! enforces between recorded steps, and the failures a journal write reports.
3
4use coven_protocol::store_commit::device_join_exchange::DeviceJoinAbandonment;
5use coven_protocol::store_commit::device_join_journal::{
6    DeviceJoinJournalRecord, DeviceJoinRoleProgress, JoinerJoinProgress, OwnerJoinProgress,
7};
8
9/// A journal transition that contradicts the durable record. Workflow errors
10/// wrap it at the operation boundary.
11#[derive(Debug, thiserror::Error)]
12pub enum DeviceJoinJournalError {
13    #[error("device join journal transition is not the declared adjacent transition")]
14    NonAdjacentJournalTransition,
15    #[error("device join journal has a different durable value for this role and attempt")]
16    JournalConflict,
17    #[error("device join journal: {0}")]
18    Serialization(#[from] serde_json::Error),
19    #[error("device join journal: {0}")]
20    Database(#[from] crate::DbError),
21}
22
23/// The progress values a role's first record may hold.
24pub fn validate_initial_progress(
25    progress: &DeviceJoinRoleProgress,
26) -> Result<(), DeviceJoinJournalError> {
27    if matches!(
28        progress,
29        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(_))
30            | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::OfferReceived(_))
31    ) {
32        Ok(())
33    } else {
34        Err(DeviceJoinJournalError::NonAdjacentJournalTransition)
35    }
36}
37
38pub fn require_initial(record: &DeviceJoinJournalRecord) -> Result<(), DeviceJoinJournalError> {
39    validate_initial_progress(&record.progress)
40}
41
42pub fn validate_successor(
43    previous: &DeviceJoinJournalRecord,
44    next: &DeviceJoinJournalRecord,
45) -> Result<(), DeviceJoinJournalError> {
46    if previous.attempt_id != next.attempt_id {
47        return Err(DeviceJoinJournalError::JournalConflict);
48    }
49    validate_transition(&previous.progress, &next.progress)
50}
51
52/// The joiner record an observed abandonment advances to, or `None` when the
53/// journal already holds that exact abandonment.
54/// Whether `record` is a joiner row an abandonment may retire.
55///
56/// The joining device keeps no abandoned state: accepting an abandonment
57/// deletes the row, so afterwards its absence is the whole answer and there is
58/// no state left for a second acceptance to compare against. Only the two
59/// waiting states can be abandoned — past them the device has been approved and
60/// holds storage access, which an abandonment does not take back.
61pub fn joiner_abandonment_retires(
62    record: &DeviceJoinJournalRecord,
63    abandonment: &DeviceJoinAbandonment,
64) -> Result<(), DeviceJoinJournalError> {
65    if record.attempt_id != abandonment.abandonment.attempt_id {
66        return Err(DeviceJoinJournalError::JournalConflict);
67    }
68    match &*record.progress {
69        DeviceJoinRoleProgress::Joiner(
70            JoinerJoinProgress::AccessRequested(_) | JoinerJoinProgress::ApprovalReceived(_),
71        ) => Ok(()),
72        _ => Err(DeviceJoinJournalError::JournalConflict),
73    }
74}
75
76fn validate_transition(
77    previous: &DeviceJoinRoleProgress,
78    next: &DeviceJoinRoleProgress,
79) -> Result<(), DeviceJoinJournalError> {
80    let adjacent = match (previous, next) {
81        (DeviceJoinRoleProgress::Owner(previous), DeviceJoinRoleProgress::Owner(next)) => {
82            owner_adjacent(previous, next)
83        }
84        (DeviceJoinRoleProgress::Joiner(previous), DeviceJoinRoleProgress::Joiner(next)) => {
85            joiner_adjacent(previous, next)
86        }
87        _ => false,
88    };
89    if adjacent {
90        Ok(())
91    } else {
92        Err(DeviceJoinJournalError::NonAdjacentJournalTransition)
93    }
94}
95
96/// The admitting device's steps, in one chain. One device answers the access
97/// request, prepares the storage grant, signs the approval, registers the
98/// joining device and activates it, so every step below follows the previous
99/// one on the same journal row.
100///
101/// The chain ends where it ends. Up to the attempt commit the admitting device
102/// can still give up, and abandonment says so; past it there is nothing to take
103/// back, because approving the join is what granted the joining device storage
104/// access and undoing that is member removal with a key rotation.
105fn owner_adjacent(previous: &OwnerJoinProgress, next: &OwnerJoinProgress) -> bool {
106    if let (
107        OwnerJoinProgress::AccessRequested(request),
108        OwnerJoinProgress::ApprovalPrepared(approval),
109    ) = (previous, next)
110    {
111        return approval.request.as_ref() == request
112            && matches!(
113                approval.admission,
114                coven_protocol::store_commit::device_join_exchange::DeviceProviderAdmission::SamePrincipal
115            );
116    }
117    if let (
118        OwnerJoinProgress::ProviderReady(ready),
119        OwnerJoinProgress::Completed(
120            coven_protocol::store_commit::device_join_exchange::DeviceProviderAdmissionCompletion::SamePrincipal {
121                bootstrap,
122            },
123        ),
124    ) = (previous, next)
125    {
126        return ready == bootstrap.as_ref();
127    }
128    if let OwnerJoinProgress::StorePublicationPrepared(prepared) = previous {
129        return prepared
130            .validates_accepted_progress(prepared_operation_attempt_id(&prepared.operation), next);
131    }
132    if let OwnerJoinProgress::StorePublicationPrepared(prepared) = next {
133        return owner_publication_follows(previous, &prepared.operation);
134    }
135    matches!(
136        (previous, next),
137        (
138            OwnerJoinProgress::Offered(_),
139            OwnerJoinProgress::AccessRequested(_)
140        ) | (
141            OwnerJoinProgress::AccessGrantActivated { .. },
142            OwnerJoinProgress::ApprovalPrepared(_)
143        ) | (
144            OwnerJoinProgress::ApprovalPrepared(_),
145            OwnerJoinProgress::RegistrationRequested(_)
146        ) | (
147            OwnerJoinProgress::SamePrincipalActivated { .. },
148            OwnerJoinProgress::SamePrincipalCompleted { .. }
149        ) | (
150            OwnerJoinProgress::AttemptActivated(_),
151            OwnerJoinProgress::ChallengeCreateIntent(_)
152        ) | (
153            OwnerJoinProgress::ChallengeCreateIntent(_),
154            OwnerJoinProgress::ProviderReady(_)
155        ) | (
156            OwnerJoinProgress::ProviderReady(_),
157            OwnerJoinProgress::ResponseObserved(_)
158        ) | (
159            OwnerJoinProgress::ResponseObserved(_),
160            OwnerJoinProgress::Completed(_)
161        )
162    )
163}
164
165fn owner_publication_follows(
166    previous: &OwnerJoinProgress,
167    operation: &coven_protocol::store_commit::device_join_journal::OwnerJoinPublication,
168) -> bool {
169    use coven_protocol::store_commit::device_join_journal::OwnerJoinPublication;
170
171    match (previous, operation) {
172        (
173            OwnerJoinProgress::AccessRequested(previous),
174            OwnerJoinPublication::ProviderAccessGrant { request, .. },
175        ) => previous == request,
176        (
177            OwnerJoinProgress::RegistrationRequested(previous),
178            OwnerJoinPublication::Attempt { request }
179            | OwnerJoinPublication::SamePrincipalActivation { request },
180        ) => previous == request,
181        (
182            OwnerJoinProgress::Completed(previous),
183            OwnerJoinPublication::JoinActivation { completion },
184        ) => previous == completion,
185        (previous, OwnerJoinPublication::Abandonment { offer, .. }) => {
186            let durable_offer = match previous {
187                OwnerJoinProgress::Offered(durable) => Some(durable),
188                OwnerJoinProgress::AccessRequested(request) => Some(request.offer.as_ref()),
189                OwnerJoinProgress::AccessGrantActivated { request, .. } => {
190                    Some(request.offer.as_ref())
191                }
192                OwnerJoinProgress::ApprovalPrepared(approval) => {
193                    Some(approval.request.offer.as_ref())
194                }
195                OwnerJoinProgress::RegistrationRequested(request) => {
196                    Some(request.approval().request.offer.as_ref())
197                }
198                _ => None,
199            };
200            durable_offer == Some(offer)
201        }
202        _ => false,
203    }
204}
205
206fn prepared_operation_attempt_id(
207    operation: &coven_protocol::store_commit::device_join_journal::OwnerJoinPublication,
208) -> coven_protocol::store_commit::DeviceJoinAttemptId {
209    use coven_protocol::store_commit::device_join_journal::OwnerJoinPublication;
210    match operation {
211        OwnerJoinPublication::ProviderAccessGrant { request, .. } => request.offer.attempt_id,
212        OwnerJoinPublication::Attempt { request }
213        | OwnerJoinPublication::SamePrincipalActivation { request } => {
214            request.approval().request.offer.attempt_id
215        }
216        OwnerJoinPublication::Abandonment { offer, .. } => offer.attempt_id,
217        OwnerJoinPublication::JoinActivation { completion } => completion.attempt_id(),
218    }
219}
220
221fn joiner_adjacent(previous: &JoinerJoinProgress, next: &JoinerJoinProgress) -> bool {
222    matches!(
223        (previous, next),
224        (
225            JoinerJoinProgress::OfferReceived(_),
226            JoinerJoinProgress::AccessRequested(_)
227        ) | (
228            JoinerJoinProgress::AccessRequested(_),
229            JoinerJoinProgress::ApprovalReceived(_)
230        ) | (
231            JoinerJoinProgress::ApprovalReceived(_),
232            JoinerJoinProgress::RegistrationPrepared(_)
233        ) | (
234            JoinerJoinProgress::RegistrationPrepared(_),
235            JoinerJoinProgress::Ready(_)
236        ) | (
237            JoinerJoinProgress::Ready(_),
238            JoinerJoinProgress::ActivationObserved { .. }
239        )
240    )
241}