coven_database/store/store_session/
owner_recovery_publication.rs1use super::*;
2use crate::*;
3use coven_protocol::store_commit::{
4 StoreBatchCommit, StoreCommitCoord, StoreDeviceRegistration,
5 StoreDeviceRegistrationActivationRef, StoreDeviceRegistrationOrigin, VerifiedStoreBatchCommit,
6};
7use rusqlite::OptionalExtension;
8
9#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10#[serde(deny_unknown_fields)]
11struct DurableOwnerRecoveryPublication {
12 commit: DurablePreparedProtocolObject,
13 head: DurablePreparedProtocolObject,
14 history_evidence: coven_protocol::store_commit::RetainedMergeCommitEvidence,
15}
16
17pub(super) fn complete_owner_recovery_publication_on(
18 transaction: &rusqlite::Transaction<'_>,
19 commit: &VerifiedStoreBatchCommit,
20 head: &coven_protocol::store_commit::StoreDeviceHead,
21 head_object: &coven_protocol::objects::ExactObjectRef,
22) -> Result<(), DbError> {
23 if complete_matching_owner_recovery_publication_on(transaction, commit, head, head_object)? {
24 return Ok(());
25 }
26 Err(DbError::Message(
27 "completed Owner recovery has no exact publication journal".into(),
28 ))
29}
30
31pub(super) fn complete_matching_owner_recovery_publication_on(
32 transaction: &rusqlite::Transaction<'_>,
33 commit: &VerifiedStoreBatchCommit,
34 head: &coven_protocol::store_commit::StoreDeviceHead,
35 head_object: &coven_protocol::objects::ExactObjectRef,
36) -> Result<bool, DbError> {
37 let stored: Option<(String, String)> = transaction
38 .query_row(
39 "SELECT registration_hash, publication
40 FROM local_owner_recovery_publication WHERE singleton = 1",
41 [],
42 |row| Ok((row.get(0)?, row.get(1)?)),
43 )
44 .optional()
45 .map_err(DbError::from)?;
46 let Some(stored) = stored else {
47 return Ok(false);
48 };
49 if stored.0 != commit.author_registration.registration_hash.to_string() {
50 return Ok(false);
51 }
52 let durable: DurableOwnerRecoveryPublication = serde_json::from_str(&stored.1)
53 .map_err(|error| DbError::context("parse completed Owner recovery publication", error))?;
54 if durable.commit.semantic_bytes() != commit.value().to_bytes()
55 || durable.commit.prepared().reference() != &commit.reference().object
56 || durable.head.semantic_bytes() != head.to_bytes()
57 || durable.head.prepared().reference() != head_object
58 {
59 return Err(DbError::Message(
60 "completed Owner recovery differs from its exact publication journal".into(),
61 ));
62 }
63 let deleted = transaction
64 .execute(
65 "DELETE FROM local_owner_recovery_publication
66 WHERE singleton = 1 AND registration_hash = ?1 AND publication = ?2",
67 (&stored.0, &stored.1),
68 )
69 .map_err(DbError::from)?;
70 if deleted != 1 {
71 return Err(DbError::Message(
72 "Owner recovery publication changed during completion".into(),
73 ));
74 }
75 Ok(true)
76}
77
78impl DurableOwnerRecoveryPublication {
79 fn from_publication(publication: OwnerRecoveryPublication) -> Result<Self, DbError> {
80 if publication.commit.bytes != publication.commit.value.value().to_bytes()
81 || publication.head.bytes != publication.head.value.to_bytes()
82 {
83 return Err(DbError::Message(
84 "Owner recovery publication carries noncanonical semantic bytes".into(),
85 ));
86 }
87 Ok(Self {
88 commit: DurablePreparedProtocolObject::new(
89 publication.commit.bytes,
90 publication.commit.prepared,
91 ),
92 head: DurablePreparedProtocolObject::new(
93 publication.head.bytes,
94 publication.head.prepared,
95 ),
96 history_evidence: publication.history_evidence,
97 })
98 }
99}
100
101impl StoreSession<'_> {
102 fn verify_owner_recovery_publication(
103 &mut self,
104 durable: DurableOwnerRecoveryPublication,
105 ) -> Result<(OwnerRecoveryPublication, ObjectHash), DbError> {
106 let local = self.local_store_device_registration()?.ok_or_else(|| {
107 DbError::Message("Owner recovery registration journal is absent".into())
108 })?;
109 if local.state != LocalDeviceRegistrationState::Created {
110 return Err(DbError::Message(
111 "Owner recovery publication requires created registration objects".into(),
112 ));
113 }
114 let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
115 let root = self
116 .verified_store_authority
117 .required_root_authority_on(records)?;
118 let registration =
119 StoreDeviceRegistration::parse_at(&local.registration_bytes, &root, local.device_id)
120 .map_err(|error| DbError::context("Owner recovery local registration", error))?;
121 let registration_ref =
122 coven_protocol::store_commit::StoreDeviceRegistrationRef::from_registration(
123 ®istration,
124 local.prepared.reference().clone(),
125 );
126 if registration_ref.registration_hash != local.registration_hash {
127 return Err(DbError::Message(
128 "Owner recovery local registration hash differs from its exact reference".into(),
129 ));
130 }
131 let StoreDeviceRegistrationOrigin::Recovery {
132 recovery_id,
133 recovery_slot,
134 owner_grant,
135 } = ®istration.origin
136 else {
137 return Err(DbError::Message(
138 "Owner recovery publication has a non-recovery registration".into(),
139 ));
140 };
141
142 durable
143 .commit
144 .prepared()
145 .reference()
146 .verify(durable.commit.prepared().stored_bytes())
147 .map_err(|error| DbError::context("Owner recovery exact commit", error))?;
148 let decoded: StoreBatchCommit = serde_json::from_slice(durable.commit.semantic_bytes())
149 .map_err(|error| DbError::context("Owner recovery commit", error))?;
150 let stream_id = coven_protocol::store_commit::StreamActivation::device_authorized_stream_id(
151 root.store_root_hash,
152 ®istration_ref,
153 coven_protocol::store_commit::StreamAnchorDomain::StoreAnnouncements,
154 );
155 let coord = StoreCommitCoord {
156 stream_id,
157 sequence: decoded.seq(),
158 };
159 let commit = VerifiedStoreBatchCommit::parse_prepared(
160 durable.commit.semantic_bytes(),
161 root.store_root_hash,
162 coord,
163 durable.commit.prepared().reference().clone(),
164 ®istration,
165 )
166 .map_err(|error| DbError::context("verify Owner recovery commit", error))?;
167 let [activation] = commit.device_registrations() else {
168 return Err(DbError::Message(
169 "Owner recovery commit must carry exactly one registration activation".into(),
170 ));
171 };
172 let StoreDeviceRegistrationActivationRef::Recovery {
173 recovery_id: activation_recovery_id,
174 node,
175 } = &activation.authority
176 else {
177 return Err(DbError::Message(
178 "Owner recovery commit carries another registration authority".into(),
179 ));
180 };
181 if commit.seq() != 1
182 || commit.author_registration != registration_ref
183 || activation.registration != registration_ref
184 || activation_recovery_id != recovery_id
185 || node.object.slot() != recovery_slot
186 || &node.owner_grant != owner_grant
187 || commit.value().to_bytes() != durable.commit.semantic_bytes()
188 {
189 return Err(DbError::Message(
190 "Owner recovery commit differs from its local recovery authority".into(),
191 ));
192 }
193 durable
194 .history_evidence
195 .validate_for(commit.reference(), commit.value())
196 .map_err(|error| DbError::context("Owner recovery history evidence", error))?;
197
198 durable
199 .head
200 .prepared()
201 .reference()
202 .verify(durable.head.prepared().stored_bytes())
203 .map_err(|error| DbError::context("Owner recovery exact head", error))?;
204 let head = coven_protocol::store_commit::StoreDeviceHead::parse_at(
205 durable.head.semantic_bytes(),
206 root.store_root_hash,
207 ®istration,
208 commit.reference(),
209 )
210 .map_err(|error| DbError::context("verify Owner recovery head", error))?;
211 let coven_protocol::store_commit::DeviceStreamAnchor::StoreAnnouncements { first_slot } =
212 ®istration.store_commits
213 else {
214 return Err(DbError::Message(
215 "Owner recovery registration has no announcement stream anchor".into(),
216 ));
217 };
218 let activation = registration
219 .store_announcement_activation(®istration_ref)
220 .map_err(|error| DbError::context("Owner recovery announcement activation", error))?
221 .activation_id();
222 if durable.head.prepared().reference().slot() != first_slot
223 || head.successor.predecessor.is_some()
224 || head.successor.activation != activation
225 || &head.successor.next_slot == first_slot
226 || head.to_bytes() != durable.head.semantic_bytes()
227 {
228 return Err(DbError::Message(
229 "Owner recovery head differs from its first announcement position".into(),
230 ));
231 }
232
233 Ok((
234 OwnerRecoveryPublication {
235 commit: ExactProtocolObject {
236 value: commit,
237 bytes: durable.commit.semantic_bytes,
238 prepared: durable.commit.prepared,
239 },
240 head: ExactProtocolObject {
241 value: head,
242 bytes: durable.head.semantic_bytes,
243 prepared: durable.head.prepared,
244 },
245 history_evidence: durable.history_evidence,
246 },
247 local.registration_hash,
248 ))
249 }
250
251 fn stage_owner_recovery_publication(
252 &mut self,
253 publication: OwnerRecoveryPublication,
254 ) -> Result<OwnerRecoveryPublication, DbError> {
255 let durable = DurableOwnerRecoveryPublication::from_publication(publication)?;
256 let (verified, registration_hash) =
257 self.verify_owner_recovery_publication(durable.clone())?;
258 let registration_hash = registration_hash.to_string();
259 let encoded = serde_json::to_string(&durable)
260 .map_err(|error| DbError::context("serialize Owner recovery publication", error))?;
261 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir)
262 .stage_owner_recovery_publication(®istration_hash, &encoded)?;
263 Ok(verified)
264 }
265
266 fn owner_recovery_publication(&mut self) -> Result<Option<OwnerRecoveryPublication>, DbError> {
267 let stored = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir)
268 .owner_recovery_publication_row()?;
269 stored
270 .map(|(registration_hash, encoded)| {
271 let durable = serde_json::from_str(&encoded)
272 .map_err(|error| DbError::context("parse Owner recovery publication", error))?;
273 let (publication, local_registration_hash) =
274 self.verify_owner_recovery_publication(durable)?;
275 if registration_hash != local_registration_hash.to_string() {
276 return Err(DbError::Message(
277 "Owner recovery publication belongs to another local registration".into(),
278 ));
279 }
280 Ok(publication)
281 })
282 .transpose()
283 }
284}
285
286impl StoreDatabase {
287 pub async fn stage_owner_recovery_publication(
288 &self,
289 publication: OwnerRecoveryPublication,
290 ) -> Result<OwnerRecoveryPublication, DbError> {
291 self.call_store(move |session| session.stage_owner_recovery_publication(publication))
292 .await
293 }
294
295 pub async fn owner_recovery_publication(
296 &self,
297 ) -> Result<Option<OwnerRecoveryPublication>, DbError> {
298 self.call_store(|session| session.owner_recovery_publication())
299 .await
300 }
301}