Skip to main content

coven_database/store/store_session/
device_continuation.rs

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            &registration,
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                &registration_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(&registration)
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            &registration,
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        // The chain runs from the device's current head back to its initial
133        // ack. The code's latest ack is where the stream stood at export; the
134        // head is at or past it, and the chain must pass through it exactly.
135        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        // `latest_snapshot` is the head of the device's published stream as
159        // the restore found it on the provider; the code's cursor is where the
160        // stream stood when the code was exported, so the head is at or past
161        // it, never behind it.
162        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                    &registration,
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() != &registration {
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        // The chain's first entry is the device's current head, at or past the
415        // code's cursor (the caller verified that).
416        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}