coven_database/store/store_session/
device_join_publication.rs1use super::{StoreDatabase, StoreSession};
2use crate::{
3 persist_exact_remote_object_on, ActiveStorePublication, ActiveStorePublicationOwner, DbError,
4};
5use coven_protocol::store_commit::device_join_journal::{
6 DeviceJoinJournalRecord, DeviceJoinRole, DeviceJoinRoleProgress, OwnerJoinProgress,
7 PreparedOwnerJoinPublication,
8};
9
10impl StoreSession<'_> {
11 fn prepare_owner_device_join_publication(
12 &mut self,
13 previous: DeviceJoinJournalRecord,
14 prepared: PreparedOwnerJoinPublication,
15 ) -> Result<DeviceJoinJournalRecord, DbError> {
16 if previous.progress.role() != DeviceJoinRole::Owner {
17 return Err(DbError::Message(
18 "device join publication belongs to a non-Owner journal".to_string(),
19 ));
20 }
21 prepared
22 .validate_for(previous.attempt_id)
23 .map_err(|error| DbError::context("prepared owner device join publication", error))?;
24 let next = DeviceJoinJournalRecord {
25 attempt_id: previous.attempt_id,
26 progress: Box::new(DeviceJoinRoleProgress::Owner(
27 OwnerJoinProgress::StorePublicationPrepared(prepared.clone()),
28 )),
29 };
30 crate::store::device_join_journal::validate_successor(&previous, &next).map_err(
31 |error| DbError::Message(format!("prepare owner device join publication: {error}")),
32 )?;
33 let active = ActiveStorePublication::for_commit(
34 ActiveStorePublicationOwner::DeviceJoin(previous.attempt_id),
35 &prepared.candidate,
36 )?;
37 let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
38 let key = previous.store_key();
39 let expected = serde_json::to_string(&previous)
40 .map_err(|error| DbError::context("serialize device join predecessor", error))?;
41 let actual = crate::required_protocol_state_on(&transaction, &key)?;
42 if actual != expected {
43 return Err(DbError::Message(
44 "device join journal changed before publication preparation".to_string(),
45 ));
46 }
47 match super::active_store_publication::claim_active_store_publication_on(
48 &transaction,
49 &active,
50 )? {
51 super::active_store_publication::ActiveStorePublicationClaim::Acquired => {}
52 super::active_store_publication::ActiveStorePublicationClaim::AlreadyOwned => {
53 return Err(DbError::Message(
54 "device join publication was active before its journal was prepared"
55 .to_string(),
56 ));
57 }
58 super::active_store_publication::ActiveStorePublicationClaim::Occupied(owner) => {
59 return Err(DbError::Message(format!(
60 "another local Store operation owns publication: {owner:?}"
61 )));
62 }
63 }
64 for remote in prepared
65 .remote_objects(previous.attempt_id)
66 .map_err(|error| DbError::context("prepare device join remote graph", error))?
67 {
68 persist_exact_remote_object_on(
69 &transaction,
70 self.store_dir,
71 &remote,
72 "device join candidate object",
73 )?;
74 }
75 let next_value = serde_json::to_string(&next)
76 .map_err(|error| DbError::context("serialize prepared device join", error))?;
77 if transaction
78 .execute(
79 "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
80 (&next_value, &key, &expected),
81 )
82 .map_err(DbError::from)?
83 != 1
84 {
85 return Err(DbError::Message(
86 "device join journal changed during publication preparation".to_string(),
87 ));
88 }
89 transaction.commit().map_err(DbError::from)?;
90 Ok(next)
91 }
92}
93
94pub(crate) fn complete_owner_device_join_publication_on(
95 transaction: &rusqlite::Transaction<'_>,
96 candidate: &coven_protocol::store_commit::StoreBatchCommitRef,
97 completion: Option<&coven_protocol::membership_mutation::StoreMembershipJournalCompletion>,
98) -> Result<(), DbError> {
99 let active = super::active_store_publication::load_active_store_publication_on(transaction)?
100 .ok_or_else(|| {
101 DbError::Message(format!(
102 "accepted device join candidate {candidate:?} has no active publication"
103 ))
104 })?;
105 let ActiveStorePublicationOwner::DeviceJoin(attempt_id) = active.owner() else {
106 return Ok(());
107 };
108 if !matches!(
109 &active.attempt()?.entry.payload,
110 coven_protocol::store_commit::StorePublicationPayload::Commit(reference)
111 if reference == candidate
112 ) {
113 return Err(DbError::Message(
114 "accepted device join candidate differs from its active publication".to_string(),
115 ));
116 }
117 let key = DeviceJoinJournalRecord::store_key_for(*attempt_id, DeviceJoinRole::Owner);
118 let current_value = crate::required_protocol_state_on(transaction, &key)?;
119 let current: DeviceJoinJournalRecord = serde_json::from_str(¤t_value)
120 .map_err(|error| DbError::context("prepared device join journal", error))?;
121 let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::StorePublicationPrepared(prepared)) =
122 &*current.progress
123 else {
124 return Err(DbError::Message(
125 "active device join publication has no prepared owner journal".to_string(),
126 ));
127 };
128 if prepared.candidate.reference != *candidate
129 || active.commit_reservation()
130 != Some((
131 &prepared.candidate.commit.write_id,
132 &prepared.candidate.commit.author_registration,
133 &candidate.coord,
134 ))
135 {
136 return Err(DbError::Message(
137 "active device join publication differs from its prepared journal".to_string(),
138 ));
139 }
140 let winning = active.attempt()?;
141 if matches!(
142 prepared.operation,
143 coven_protocol::store_commit::device_join_journal::OwnerJoinPublication::SamePrincipalActivation { .. }
144 ) {
145 let Some(coven_protocol::membership_mutation::StoreMembershipJournalCompletion::DeviceJoin {
146 remote_objects,
147 }) = completion else {
148 return Err(DbError::Message(
149 "same-principal handoff has no finalized registration authority".into(),
150 ));
151 };
152 let proof = prepared.candidate.history_evidence.membership_proof.as_ref()
153 .ok_or_else(|| DbError::Message("same-principal handoff has no exact authority head".into()))?;
154 let results = remote_objects.iter().filter_map(|record| {
155 let coven_protocol::remote_object::RemoteObjectRecord::RetainedAuthority(value) = record else {
156 return None;
157 };
158 match &value.identity.domain {
159 coven_protocol::remote_object::RetainedAuthorityObjectDomain::MembershipHeadAcceptance { head, publication }
160 if head == &proof.head => Some(publication),
161 _ => None,
162 }
163 }).collect::<Vec<_>>();
164 if results.as_slice() != [&winning.reference()?] {
165 return Err(DbError::Message(
166 "same-principal handoff differs from its exact winning publication".into(),
167 ));
168 }
169 }
170 let accepted = DeviceJoinJournalRecord {
171 attempt_id: *attempt_id,
172 progress: Box::new(DeviceJoinRoleProgress::Owner(
173 prepared
174 .accepted_progress(*attempt_id, winning.replacement.clone())
175 .map_err(|error| DbError::context("accepted device join progress", error))?,
176 )),
177 };
178 crate::store::device_join_journal::validate_successor(¤t, &accepted).map_err(
179 |error| DbError::Message(format!("complete owner device join publication: {error}")),
180 )?;
181 let accepted_value = serde_json::to_string(&accepted)
182 .map_err(|error| DbError::context("serialize accepted device join", error))?;
183 if transaction
184 .execute(
185 "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
186 (&accepted_value, &key, ¤t_value),
187 )
188 .map_err(DbError::from)?
189 != 1
190 {
191 return Err(DbError::Message(
192 "device join journal changed during publication completion".to_string(),
193 ));
194 }
195 super::active_store_publication::clear_active_store_publication_on(transaction, &active)
196}
197
198impl StoreDatabase {
199 pub async fn prepare_owner_device_join_publication(
200 &self,
201 previous: DeviceJoinJournalRecord,
202 prepared: PreparedOwnerJoinPublication,
203 ) -> Result<DeviceJoinJournalRecord, crate::store::device_join_journal::DeviceJoinJournalError>
204 {
205 self.call_store(move |session| {
206 session.prepare_owner_device_join_publication(previous, prepared)
207 })
208 .await
209 .map_err(crate::store::device_join_journal::DeviceJoinJournalError::Database)
210 }
211}