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    matches!(
129        (previous, next),
130        (
131            OwnerJoinProgress::Offered(_),
132            OwnerJoinProgress::AccessRequested(_)
133        ) | (
134            OwnerJoinProgress::AccessRequested(_),
135            OwnerJoinProgress::AccessGrantPrepared { .. }
136        ) | (
137            OwnerJoinProgress::AccessGrantPrepared { .. },
138            OwnerJoinProgress::ApprovalPrepared(_)
139        ) | (
140            OwnerJoinProgress::ApprovalPrepared(_),
141            OwnerJoinProgress::RegistrationRequested(_)
142        ) | (
143            OwnerJoinProgress::RegistrationRequested(_),
144            OwnerJoinProgress::AttemptActivated(_)
145        ) | (
146            OwnerJoinProgress::RegistrationRequested(_),
147            OwnerJoinProgress::SamePrincipalActivationCreateIntent { .. }
148        ) | (
149            OwnerJoinProgress::SamePrincipalActivationCreateIntent { .. },
150            OwnerJoinProgress::SamePrincipalCompleted { .. }
151        ) | (
152            OwnerJoinProgress::Offered(_),
153            OwnerJoinProgress::AbandonmentCreateIntent { .. }
154        ) | (
155            OwnerJoinProgress::AccessRequested(_),
156            OwnerJoinProgress::AbandonmentCreateIntent { .. }
157        ) | (
158            OwnerJoinProgress::AccessGrantPrepared { .. },
159            OwnerJoinProgress::AbandonmentCreateIntent { .. }
160        ) | (
161            OwnerJoinProgress::ApprovalPrepared(_),
162            OwnerJoinProgress::AbandonmentCreateIntent { .. }
163        ) | (
164            OwnerJoinProgress::RegistrationRequested(_),
165            OwnerJoinProgress::AbandonmentCreateIntent { .. }
166        ) | (
167            OwnerJoinProgress::AbandonmentCreateIntent { .. },
168            OwnerJoinProgress::Abandoned(_)
169        ) | (
170            OwnerJoinProgress::AttemptActivated(_),
171            OwnerJoinProgress::ChallengeCreateIntent(_)
172        ) | (
173            OwnerJoinProgress::ChallengeCreateIntent(_),
174            OwnerJoinProgress::ProviderReady(_)
175        ) | (
176            OwnerJoinProgress::ProviderReady(_),
177            OwnerJoinProgress::ResponseObserved(_)
178        ) | (
179            OwnerJoinProgress::ResponseObserved(_),
180            OwnerJoinProgress::Completed(_)
181        ) | (
182            OwnerJoinProgress::Completed(_),
183            OwnerJoinProgress::ActivationCreateIntent { .. }
184        ) | (
185            OwnerJoinProgress::ActivationCreateIntent { .. },
186            OwnerJoinProgress::ActivationPrepared { .. }
187        )
188    )
189}
190
191fn joiner_adjacent(previous: &JoinerJoinProgress, next: &JoinerJoinProgress) -> bool {
192    matches!(
193        (previous, next),
194        (
195            JoinerJoinProgress::OfferReceived(_),
196            JoinerJoinProgress::AccessRequested(_)
197        ) | (
198            JoinerJoinProgress::AccessRequested(_),
199            JoinerJoinProgress::ApprovalReceived(_)
200        ) | (
201            JoinerJoinProgress::ApprovalReceived(_),
202            JoinerJoinProgress::RegistrationPrepared(_)
203        ) | (
204            JoinerJoinProgress::RegistrationPrepared(_),
205            JoinerJoinProgress::Ready(_)
206        ) | (
207            JoinerJoinProgress::Ready(_),
208            JoinerJoinProgress::ActivationObserved { .. }
209        )
210    )
211}