1use crate::*;
2use coven_protocol::store_commit::{
3 SnapshotMeta, StoreAck, StoreAckRef, StoreDeviceRegistration, StoreDeviceRegistrationRef,
4 StoreSnapshotRef,
5};
6
7use super::*;
8
9impl StoreDatabase {
10 pub async fn latest_local_store_device_registration(
11 &self,
12 ) -> Result<Option<DurableDeviceRegistration>, DbError> {
13 self.read_local_store_device_registration(
14 "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
15 initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state \
16 FROM local_store_device_registration WHERE singleton = 1",
17 )
18 .await
19 }
20
21 pub async fn export_activated_device_continuation(
22 &self,
23 identity_signer: &coven_keys::keys::UserKeypair,
24 ) -> Result<coven_protocol::recovery::ActivatedContinuation, DbError> {
25 let durable = self
26 .latest_local_store_device_registration()
27 .await?
28 .ok_or_else(|| DbError::Message("local Store device registration is absent".into()))?;
29 let LocalDeviceRegistrationState::Activated { authority } = durable.state else {
30 return Err(DbError::Message(
31 "local Store device registration is not activated".into(),
32 ));
33 };
34 let root = self
35 .local_store_root_ref()
36 .await?
37 .ok_or_else(|| DbError::Message("local Store root hash is absent".into()))?;
38 let registration = StoreDeviceRegistration::parse_at(
39 &durable.registration_bytes,
40 &root,
41 durable.device_id,
42 )
43 .map_err(|error| DbError::context("local Store registration", error))?;
44 let registration_ref = StoreDeviceRegistrationRef::from_registration(
45 ®istration,
46 durable.prepared.reference().clone(),
47 );
48 if registration_ref.registration_hash != durable.registration_hash {
49 return Err(DbError::Message(
50 "local Store registration hash differs from its exact object".into(),
51 ));
52 }
53 let device_signer = registration
54 .device_signer(identity_signer)
55 .map_err(|error| DbError::context("local device signer", error))?;
56 let latest_ack = self
57 .latest_local_store_ack()
58 .await?
59 .ok_or_else(|| DbError::Message("local Store acknowledgement is absent".into()))?;
60 let announcement_stream_id =
61 coven_protocol::store_commit::StreamActivation::device_authorized_stream_id(
62 root.store_root_hash,
63 ®istration_ref,
64 coven_protocol::store_commit::StreamAnchorDomain::StoreAnnouncements,
65 );
66 Ok(coven_protocol::recovery::ActivatedContinuation {
67 identity_signing_secret: hex::encode(identity_signer.to_keypair_bytes()),
68 device_signing_secret: hex::encode(device_signer.to_keypair_bytes()),
69 registration: registration_ref,
70 registration_bytes: durable.registration_bytes,
71 registration_prepared: durable.prepared,
72 initial_ack: durable.initial_ack_ref,
73 initial_ack_bytes: durable.initial_ack.bytes,
74 initial_ack_prepared: durable.initial_ack.prepared,
75 activation: authority,
76 latest_ack: latest_ack.reference,
77 latest_snapshot: self
78 .latest_local_store_snapshot()
79 .await?
80 .map(|snapshot| snapshot.reference),
81 latest_position: self
82 .latest_local_store_position(announcement_stream_id)
83 .await?,
84 })
85 }
86
87 pub async fn install_activated_device_continuation(
88 &self,
89 continuation: coven_protocol::recovery::ActivatedContinuation,
90 identity_signer: &coven_keys::keys::UserKeypair,
91 device_signer: &coven_keys::keys::UserKeypair,
92 ack_chain: Vec<(StoreAckRef, StoreAck)>,
93 latest_snapshot: Option<(StoreSnapshotRef, SnapshotMeta)>,
94 ) -> Result<(), DbError> {
95 let root = self
96 .local_store_root_ref()
97 .await?
98 .ok_or_else(|| DbError::Message("local Store root hash is absent".into()))?;
99 let registration = StoreDeviceRegistration::parse_at(
100 &continuation.registration_bytes,
101 &root,
102 continuation.registration.device_id,
103 )
104 .map_err(|error| DbError::context("continued Store registration", error))?;
105 continuation
106 .registration
107 .verify_registration(®istration)
108 .map_err(DbError::from)?;
109 let derived_device = registration
110 .device_signer(identity_signer)
111 .map_err(|error| DbError::context("continued device signer", error))?;
112 if derived_device.to_keypair_bytes() != device_signer.to_keypair_bytes()
113 || continuation.registration_prepared.reference() != &continuation.registration.object
114 || continuation.initial_ack_prepared.reference() != &continuation.initial_ack.object
115 {
116 return Err(DbError::Message(
117 "continued device keys or exact registration objects differ".into(),
118 ));
119 }
120 let initial_ack = StoreAck::parse_at(
121 &continuation.initial_ack_bytes,
122 &root,
123 &continuation.initial_ack,
124 ®istration,
125 )
126 .map_err(|error| DbError::context("continued initial ack", error))?;
127 let Some((latest_ack_ref, latest_ack)) = ack_chain.first() else {
128 return Err(DbError::Message(
129 "continued acknowledgement chain is empty".into(),
130 ));
131 };
132 let pinned = ack_chain
136 .iter()
137 .find(|(reference, _)| reference.sequence == continuation.latest_ack.sequence)
138 .map(|(reference, _)| reference);
139 if initial_ack.sequence != 1
140 || initial_ack.successor.predecessor.is_some()
141 || latest_ack.registration != continuation.registration
142 || latest_ack_ref.sequence < continuation.latest_ack.sequence
143 || pinned != Some(&continuation.latest_ack)
144 || ack_chain.last().map(|(reference, _)| reference) != Some(&continuation.initial_ack)
145 || ack_chain.windows(2).any(|pair| {
146 pair[0].1.successor.predecessor.as_ref() != Some(&pair[1].0.object)
147 || pair[0].0.sequence != pair[0].1.sequence
148 || pair[1].0.sequence != pair[1].1.sequence
149 || pair[0].0.registration != pair[0].1.registration
150 || pair[1].0.registration != pair[1].1.registration
151 })
152 {
153 return Err(DbError::Message(
154 "continued acknowledgement chain differs from its exact authority".into(),
155 ));
156 }
157 let latest_successor_slot = latest_ack.successor.next_slot.clone();
158 match (&continuation.latest_snapshot, &latest_snapshot) {
163 (None, None) => {}
164 (expected, Some((reference, meta)))
165 if expected
166 .as_ref()
167 .is_none_or(|expected| expected.generation <= reference.generation) =>
168 {
169 let verified = SnapshotMeta::parse_stream_entry_at(
170 &meta.to_bytes(),
171 &root,
172 &continuation.registration,
173 ®istration,
174 reference,
175 )
176 .map_err(DbError::from)?;
177 if verified != *meta {
178 return Err(DbError::Message(
179 "continued snapshot changed during exact verification".into(),
180 ));
181 }
182 }
183 _ => {
184 return Err(DbError::Message(
185 "continued snapshot stream differs from its exact authority".into(),
186 ));
187 }
188 }
189
190 self.call_store(move |session| {
191 session.install_activated_device_continuation(
192 continuation,
193 registration,
194 ack_chain,
195 latest_snapshot,
196 latest_successor_slot,
197 )
198 })
199 .await
200 }
201}
202
203impl StoreSession<'_> {
204 fn install_activated_device_continuation(
205 &mut self,
206 continuation: coven_protocol::recovery::ActivatedContinuation,
207 registration: StoreDeviceRegistration,
208 ack_chain: Vec<(StoreAckRef, StoreAck)>,
209 latest_snapshot: Option<(StoreSnapshotRef, SnapshotMeta)>,
210 latest_successor_slot: coven_protocol::objects::ObjectSlot,
211 ) -> Result<(), DbError> {
212 let activated = self.activated_registration(&continuation.registration)?;
213 if activated.value() != ®istration {
214 return Err(DbError::Message(
215 "continued registration differs from activated Store state".into(),
216 ));
217 }
218 let conn = self.conn;
219 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
220 let stored_authority: String = tx
221 .query_row(
222 "SELECT activation_authority FROM store_device_registration_activations \
223 WHERE device_id = ?1 AND registration_hash = ?2",
224 (
225 continuation.registration.device_id.to_string(),
226 continuation.registration.registration_hash.to_string(),
227 ),
228 |row| row.get(0),
229 )
230 .map_err(DbError::from)?;
231 let stored_authority: coven_protocol::store_commit::StoreDeviceRegistrationActivation =
232 serde_json::from_str(&stored_authority)
233 .map_err(|error| DbError::context("continued activation authority", error))?;
234 if stored_authority != continuation.activation {
235 return Err(DbError::Message(
236 "continued registration has another activation authority".into(),
237 ));
238 }
239 if let Some(position) = &continuation.latest_position {
240 let stream_id = position.coord.stream_id.to_string();
241 let restored_position =
242 crate::store::materialized_commit_index::latest_position_for_device_on(
243 &tx, &stream_id,
244 )?;
245 if restored_position.as_ref() != Some(position) {
246 return Err(DbError::Message(
247 "continued device position is absent from restored history".into(),
248 ));
249 }
250 }
251 let existing_local: i64 = tx
252 .query_row(
253 "SELECT COUNT(*) FROM local_store_device_registration",
254 [],
255 |row| row.get(0),
256 )
257 .map_err(DbError::from)?;
258 let existing_ack: i64 = tx
259 .query_row("SELECT COUNT(*) FROM published_store_acks", [], |row| {
260 row.get(0)
261 })
262 .map_err(DbError::from)?;
263 let existing_snapshot = load_published_store_snapshot_on(&tx, &activated)?;
264 let existing_device = crate::get_protocol_state_on(&tx, LOCAL_DEVICE_ID_STATE_KEY)?;
265 let state = serde_json::to_string(&LocalDeviceRegistrationState::Activated {
266 authority: continuation.activation.clone(),
267 })
268 .map_err(|error| DbError::context("continued activation", error))?;
269 let expected_local = (
270 continuation.registration.device_id.to_string(),
271 continuation.registration.registration_hash.to_string(),
272 continuation.registration_bytes.clone(),
273 serde_json::to_string(&continuation.registration_prepared)
274 .map_err(|error| DbError::context("continued registration object", error))?,
275 serde_json::to_string(&continuation.initial_ack)
276 .map_err(|error| DbError::context("continued initial ack ref", error))?,
277 continuation.initial_ack_bytes.clone(),
278 serde_json::to_string(&continuation.initial_ack_prepared)
279 .map_err(|error| DbError::context("continued initial ack object", error))?,
280 state,
281 );
282 match existing_local {
283 0 => {
284 tx.execute(
285 "INSERT INTO local_store_device_registration \
286 (singleton, device_id, registration_hash, registration_bytes, \
287 prepared_object, initial_ack_ref, initial_ack_bytes, \
288 initial_ack_prepared, state) \
289 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
290 rusqlite::params![
291 expected_local.0,
292 expected_local.1,
293 expected_local.2,
294 expected_local.3,
295 expected_local.4,
296 expected_local.5,
297 expected_local.6,
298 expected_local.7,
299 ],
300 )
301 .map_err(DbError::from)?;
302 }
303 1 => {
304 let actual = tx
305 .query_row(
306 "SELECT device_id, registration_hash, registration_bytes, \
307 prepared_object, initial_ack_ref, initial_ack_bytes, \
308 initial_ack_prepared, state FROM local_store_device_registration \
309 WHERE singleton = 1",
310 [],
311 |row| {
312 Ok((
313 row.get::<_, String>(0)?,
314 row.get::<_, String>(1)?,
315 row.get::<_, Vec<u8>>(2)?,
316 row.get::<_, String>(3)?,
317 row.get::<_, String>(4)?,
318 row.get::<_, Vec<u8>>(5)?,
319 row.get::<_, String>(6)?,
320 row.get::<_, String>(7)?,
321 ))
322 },
323 )
324 .map_err(DbError::from)?;
325 if actual != expected_local {
326 return Err(DbError::Message(
327 "restored local device state differs from continuation".into(),
328 ));
329 }
330 }
331 _ => {
332 return Err(DbError::Message(
333 "restored database carries multiple local device journals".into(),
334 ));
335 }
336 }
337 match existing_ack {
338 0 => {}
339 1 => {
340 let (stored_ref, stored_successor): (String, String) = tx
341 .query_row(
342 "SELECT ack_ref, successor_slot FROM published_store_acks \
343 WHERE singleton = 1",
344 [],
345 |row| Ok((row.get(0)?, row.get(1)?)),
346 )
347 .map_err(DbError::from)?;
348 let stored_ref: StoreAckRef = serde_json::from_str(&stored_ref)
349 .map_err(|error| DbError::context("restored acknowledgement", error))?;
350 let Some((_, stored_ack)) = ack_chain
351 .iter()
352 .find(|(reference, _)| reference == &stored_ref)
353 else {
354 return Err(DbError::Message(
355 "restored acknowledgement is outside the continuation chain".into(),
356 ));
357 };
358 if stored_successor
359 != serde_json::to_string(&stored_ack.successor.next_slot)
360 .map_err(|error| DbError::context("restored ack successor", error))?
361 {
362 return Err(DbError::Message(
363 "restored acknowledgement successor differs from its signature".into(),
364 ));
365 }
366 }
367 _ => {
368 return Err(DbError::Message(
369 "restored database carries multiple local acknowledgements".into(),
370 ));
371 }
372 }
373 match (existing_snapshot, latest_snapshot.as_ref()) {
374 (None, None) => {}
375 (None, Some((reference, meta))) => {
376 let generation = i64::try_from(reference.generation).map_err(|_| {
377 DbError::Message(
378 "continued Store snapshot generation exceeds SQLite INTEGER".to_string(),
379 )
380 })?;
381 tx.execute(
382 "INSERT INTO published_store_snapshot \
383 (generation, snapshot_ref, successor_slot, meta_bytes) \
384 VALUES (?1, ?2, ?3, ?4)",
385 rusqlite::params![
386 generation,
387 serde_json::to_string(reference).map_err(|error| {
388 DbError::context("serialize continued Store snapshot ref", error)
389 })?,
390 serde_json::to_string(&meta.successor.next_slot).map_err(|error| {
391 DbError::context("serialize continued Store snapshot successor", error)
392 })?,
393 meta.to_bytes(),
394 ],
395 )
396 .map_err(DbError::from)?;
397 }
398 (Some(actual), Some((reference, meta))) => {
399 if actual.reference != *reference
400 || actual.successor_slot != meta.successor.next_slot
401 || actual.meta != *meta
402 {
403 return Err(DbError::Message(
404 "restored local snapshot stream differs from continuation".into(),
405 ));
406 }
407 }
408 (Some(_), None) => {
409 return Err(DbError::Message(
410 "restored database carries a snapshot outside the continuation".into(),
411 ));
412 }
413 }
414 let Some((head_ack_ref, _)) = ack_chain.first() else {
417 return Err(DbError::Message(
418 "continued acknowledgement chain is empty".into(),
419 ));
420 };
421 let latest_ref = serde_json::to_string(head_ack_ref)
422 .map_err(|error| DbError::context("continued latest ack", error))?;
423 let latest_successor = serde_json::to_string(&latest_successor_slot)
424 .map_err(|error| DbError::context("continued ack successor", error))?;
425 if existing_ack == 0 {
426 tx.execute(
427 "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot) \
428 VALUES (1, ?1, ?2)",
429 (&latest_ref, &latest_successor),
430 )
431 .map_err(DbError::from)?;
432 } else {
433 tx.execute(
434 "UPDATE published_store_acks SET ack_ref = ?1, successor_slot = ?2 \
435 WHERE singleton = 1",
436 (&latest_ref, &latest_successor),
437 )
438 .map_err(DbError::from)?;
439 }
440 match existing_device {
441 Some(existing) if existing == continuation.registration.device_id.to_string() => {}
442 Some(_) => {
443 return Err(DbError::Message(
444 "restored local device id differs from continuation".into(),
445 ));
446 }
447 None => {
448 tx.execute(
449 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
450 (
451 LOCAL_DEVICE_ID_STATE_KEY,
452 continuation.registration.device_id.to_string(),
453 ),
454 )
455 .map_err(DbError::from)?;
456 }
457 }
458 tx.commit().map_err(DbError::from)
459 }
460}