Skip to main content

coven_database/store/store_session/
device_continuation.rs

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