Skip to main content

coven_database/store/store_session/
device_registration_journal.rs

1use crate::*;
2use coven_protocol::store_commit::{StoreAck, StoreDeviceRegistration, StoreDeviceRegistrationRef};
3use rusqlite::OptionalExtension;
4
5use super::*;
6
7/// One local device registration together with the first acknowledgement that
8/// anchors its stream, checked against each other before any of it reaches a
9/// column. Every way a registration enters the journal — a joining device's
10/// staged registration, an existing founder's installation, an Owner recovery —
11/// builds one of these first, so the journal cannot hold a graph whose
12/// references disagree with the objects beside them.
13pub(crate) struct LocalRegistrationRecord {
14    registration: ExactProtocolObject<StoreDeviceRegistration>,
15    initial_ack_ref: StoreAckRef,
16    initial_ack: ExactProtocolObject<StoreAck>,
17    reference: StoreDeviceRegistrationRef,
18}
19
20impl LocalRegistrationRecord {
21    /// Each reference must name the exact object beside it, and both the
22    /// registration and its acknowledgement must serialize back to the bytes
23    /// they carry. `subject` names the graph in any refusal.
24    pub(crate) fn checked(
25        registration: ExactProtocolObject<StoreDeviceRegistration>,
26        initial_ack_ref: StoreAckRef,
27        initial_ack: ExactProtocolObject<StoreAck>,
28        subject: &str,
29    ) -> Result<Self, DbError> {
30        let reference = StoreDeviceRegistrationRef::from_registration(
31            &registration.value,
32            registration.prepared.reference().clone(),
33        );
34        if registration.value.to_bytes() != registration.bytes
35            || initial_ack.value.to_bytes() != initial_ack.bytes
36            || &initial_ack_ref.object != initial_ack.prepared.reference()
37            || initial_ack_ref.ack_hash != initial_ack.value.ack_hash()
38            || initial_ack_ref.registration != reference
39            || initial_ack_ref.sequence != initial_ack.value.sequence
40            || initial_ack.value.registration != reference
41        {
42            return Err(DbError::Message(format!(
43                "{subject} contains mismatched exact objects"
44            )));
45        }
46        Ok(Self {
47            registration,
48            initial_ack_ref,
49            initial_ack,
50            reference,
51        })
52    }
53
54    /// The same graph, for a device whose acknowledgement stream begins here:
55    /// the acknowledgement is the first one and has no predecessor.
56    pub(crate) fn checked_at_stream_start(
57        registration: ExactProtocolObject<StoreDeviceRegistration>,
58        initial_ack_ref: StoreAckRef,
59        initial_ack: ExactProtocolObject<StoreAck>,
60        subject: &str,
61    ) -> Result<Self, DbError> {
62        let record = Self::checked(registration, initial_ack_ref, initial_ack, subject)?;
63        if record.initial_ack_ref.sequence != 1
64            || record.initial_ack.value.successor.predecessor.is_some()
65        {
66            return Err(DbError::Message(format!(
67                "{subject} does not start its acknowledgement stream"
68            )));
69        }
70        Ok(record)
71    }
72
73    pub(crate) fn reference(&self) -> &StoreDeviceRegistrationRef {
74        &self.reference
75    }
76
77    pub(crate) fn registration(&self) -> &StoreDeviceRegistration {
78        &self.registration.value
79    }
80
81    pub(crate) fn device_id(&self) -> String {
82        self.reference.device_id.to_string()
83    }
84
85    /// The Store root this database is installed under, refusing a graph signed
86    /// against a different one.
87    pub(crate) fn require_installed_store_root(
88        &self,
89        root: &coven_protocol::store_commit::StoreRootRef,
90        subject: &str,
91    ) -> Result<(), DbError> {
92        if &self.registration.value.store_root != root {
93            return Err(DbError::Message(format!(
94                "{subject} belongs to another Store root"
95            )));
96        }
97        Ok(())
98    }
99
100    /// The journal's seven object columns, in table order.
101    pub(crate) fn columns(
102        &self,
103        subject: &str,
104    ) -> Result<PreparedLocalDeviceRegistrationRow, DbError> {
105        Ok((
106            self.device_id(),
107            self.reference.registration_hash.to_string(),
108            self.registration.bytes.clone(),
109            encode(&self.registration.prepared, subject, "registration object")?,
110            encode(&self.initial_ack_ref, subject, "acknowledgement ref")?,
111            self.initial_ack.bytes.clone(),
112            encode(
113                &self.initial_ack.prepared,
114                subject,
115                "acknowledgement object",
116            )?,
117        ))
118    }
119
120    /// The published-acknowledgement columns that name this device's first
121    /// acknowledgement and the slot its successor will occupy.
122    pub(crate) fn published_ack_columns(&self, subject: &str) -> Result<(String, String), DbError> {
123        Ok((
124            encode(&self.initial_ack_ref, subject, "acknowledgement ref")?,
125            encode(
126                &self.initial_ack.value.successor.next_slot,
127                subject,
128                "acknowledgement successor",
129            )?,
130        ))
131    }
132}
133
134fn encode<T: serde::Serialize>(value: &T, subject: &str, what: &str) -> Result<String, DbError> {
135    serde_json::to_string(value)
136        .map_err(|error| DbError::context(format!("serialize {subject} {what}"), error))
137}
138
139impl StoreSession<'_> {
140    fn stage_local_store_device_registration(
141        &mut self,
142        record: LocalRegistrationRecord,
143        initial_state: LocalDeviceRegistrationState,
144        subject: &str,
145    ) -> Result<(), DbError> {
146        let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
147        let root = self
148            .verified_store_authority
149            .required_root_authority_on(records)?;
150        record.require_installed_store_root(&root, subject)?;
151        let expected = record.columns(subject)?;
152        let existing: Option<PreparedLocalDeviceRegistrationRow> = self
153            .conn
154            .query_row(
155                "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
156                        initial_ack_ref, initial_ack_bytes, initial_ack_prepared \
157                 FROM local_store_device_registration WHERE singleton = 1",
158                [],
159                |row| {
160                    Ok((
161                        row.get(0)?,
162                        row.get(1)?,
163                        row.get(2)?,
164                        row.get(3)?,
165                        row.get(4)?,
166                        row.get(5)?,
167                        row.get(6)?,
168                    ))
169                },
170            )
171            .optional()
172            .map_err(DbError::from)?;
173        match existing {
174            Some(existing) if existing == expected => {
175                let state: String = self
176                    .conn
177                    .query_row(
178                        "SELECT state FROM local_store_device_registration WHERE singleton = 1",
179                        [],
180                        |row| row.get(0),
181                    )
182                    .map_err(DbError::from)?;
183                let state: LocalDeviceRegistrationState = serde_json::from_str(&state)
184                    .map_err(|error| DbError::context("parse local registration state", error))?;
185                let valid = match (&initial_state, &state) {
186                    (LocalDeviceRegistrationState::Prepared, _) => true,
187                    (
188                        LocalDeviceRegistrationState::RegistrationActivated {
189                            authority: expected,
190                        },
191                        LocalDeviceRegistrationState::RegistrationActivated { authority: actual }
192                        | LocalDeviceRegistrationState::Activated { authority: actual },
193                    ) => expected == actual,
194                    _ => false,
195                };
196                if !valid {
197                    return Err(DbError::Message(
198                        "local registration journal has a different publication state".to_string(),
199                    ));
200                }
201                Ok(())
202            }
203            Some(_) => Err(DbError::Message(
204                "local registration journal already owns different exact objects".to_string(),
205            )),
206            None => self
207                .conn
208                .execute(
209                    "INSERT INTO local_store_device_registration \
210                     (singleton, device_id, registration_hash, registration_bytes, \
211                      prepared_object, initial_ack_ref, initial_ack_bytes, \
212                      initial_ack_prepared, state) \
213                     VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
214                    rusqlite::params![
215                        expected.0,
216                        expected.1,
217                        expected.2,
218                        expected.3,
219                        expected.4,
220                        expected.5,
221                        expected.6,
222                        serde_json::to_string(&initial_state).map_err(|error| {
223                            DbError::context("serialize local registration state", error)
224                        })?,
225                    ],
226                )
227                .map(|_| ())
228                .map_err(DbError::from),
229        }
230    }
231
232    fn stage_activated_local_store_device_registration(
233        &mut self,
234        record: LocalRegistrationRecord,
235        authority: coven_protocol::store_commit::StoreDeviceRegistrationActivation,
236        subject: &str,
237    ) -> Result<(), DbError> {
238        let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
239        let root = self
240            .verified_store_authority
241            .required_root_authority_on(records)?;
242        record.require_installed_store_root(&root, subject)?;
243        let installed: (String, Vec<u8>, String, String) = self
244            .conn
245            .query_row(
246                "SELECT registration_hash, registration_bytes, registration_object, \
247                        activation_authority \
248                 FROM store_device_registration_activations WHERE device_id = ?1",
249                [record.device_id()],
250                |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
251            )
252            .map_err(DbError::from)?;
253        let expected = (
254            record.reference().registration_hash.to_string(),
255            record.registration.bytes.clone(),
256            encode(record.reference(), subject, "registration ref")?,
257            encode(&authority, subject, "activation authority")?,
258        );
259        if installed != expected {
260            return Err(DbError::Message(
261                "installed activation differs from the local registration graph".to_string(),
262            ));
263        }
264        self.stage_local_store_device_registration(
265            record,
266            LocalDeviceRegistrationState::RegistrationActivated { authority },
267            subject,
268        )
269    }
270
271    fn install_existing_local_founder_device(
272        &mut self,
273        record: LocalRegistrationRecord,
274        subject: &str,
275    ) -> Result<(), DbError> {
276        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
277        let store_transaction =
278            crate::store::store_session::StoreTransaction::new(&tx, self.store_dir);
279        let root = store_transaction.required_root_authority(self.verified_store_authority)?;
280        record.require_installed_store_root(&root, subject)?;
281        let coven_protocol::store_commit::StoreDeviceRegistrationOrigin::Founder { .. } =
282            &record.registration().origin
283        else {
284            return Err(DbError::Message(
285                "existing local founder device has a non-founder origin".to_string(),
286            ));
287        };
288        let activated = store_transaction.activated_registration(
289            self.verified_store_authority,
290            &root,
291            record.reference(),
292        )?;
293        if activated != *record.registration() {
294            return Err(DbError::Message(
295                "existing local founder device differs from its installed activation".to_string(),
296            ));
297        }
298        let authority = coven_protocol::store_commit::StoreDeviceRegistrationActivation::Founder {
299            root: root.clone(),
300        };
301        let objects = record.columns(subject)?;
302        let expected = (
303            objects.0,
304            objects.1,
305            objects.2,
306            objects.3,
307            objects.4,
308            objects.5,
309            objects.6,
310            encode(
311                &LocalDeviceRegistrationState::Activated { authority },
312                subject,
313                "registration state",
314            )?,
315        );
316        tx.execute(
317            "INSERT INTO local_store_device_registration
318                 (singleton, device_id, registration_hash, registration_bytes,
319                  prepared_object, initial_ack_ref, initial_ack_bytes,
320                  initial_ack_prepared, state)
321                 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
322                 ON CONFLICT(singleton) DO NOTHING",
323            rusqlite::params![
324                &expected.0,
325                &expected.1,
326                &expected.2,
327                &expected.3,
328                &expected.4,
329                &expected.5,
330                &expected.6,
331                &expected.7,
332            ],
333        )
334        .map_err(DbError::from)?;
335        let stored: LocalDeviceRegistrationJournalRow = tx
336            .query_row(
337                "SELECT device_id, registration_hash, registration_bytes, prepared_object,
338                            initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state
339                     FROM local_store_device_registration WHERE singleton = 1",
340                [],
341                |row| {
342                    Ok((
343                        row.get(0)?,
344                        row.get(1)?,
345                        row.get(2)?,
346                        row.get(3)?,
347                        row.get(4)?,
348                        row.get(5)?,
349                        row.get(6)?,
350                        row.get(7)?,
351                    ))
352                },
353            )
354            .map_err(DbError::from)?;
355        if stored != expected {
356            return Err(DbError::Message(
357                "existing local founder journal owns different exact objects".to_string(),
358            ));
359        }
360        let published_ack = record.published_ack_columns(subject)?;
361        tx.execute(
362            "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot)
363                 VALUES (1, ?1, ?2) ON CONFLICT(singleton) DO NOTHING",
364            (&published_ack.0, &published_ack.1),
365        )
366        .map_err(DbError::from)?;
367        let stored_ack: (String, String) = tx
368            .query_row(
369                "SELECT ack_ref, successor_slot FROM published_store_acks WHERE singleton = 1",
370                [],
371                |row| Ok((row.get(0)?, row.get(1)?)),
372            )
373            .map_err(DbError::from)?;
374        if stored_ack != published_ack {
375            return Err(DbError::Message(
376                "existing local founder acknowledgement differs from exact cloud state".to_string(),
377            ));
378        }
379        tx.execute(
380            "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)
381                 ON CONFLICT(key) DO NOTHING",
382            (LOCAL_DEVICE_ID_STATE_KEY, &expected.0),
383        )
384        .map_err(DbError::from)?;
385        let stored_device_id = crate::required_protocol_state_on(&tx, LOCAL_DEVICE_ID_STATE_KEY)?;
386        if stored_device_id != expected.0 {
387            return Err(DbError::Message(
388                "existing local founder device id conflicts with installed state".to_string(),
389            ));
390        }
391        tx.commit().map_err(DbError::from)
392    }
393
394    fn stage_owner_recovery_registration(
395        &mut self,
396        record: LocalRegistrationRecord,
397        activation: coven_protocol::store_commit::StoreDeviceRegistrationActivation,
398        subject: &str,
399    ) -> Result<bool, DbError> {
400        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
401        let store_transaction =
402            crate::store::store_session::StoreTransaction::new(&tx, self.store_dir);
403        let root = store_transaction.required_root_authority(self.verified_store_authority)?;
404        record.require_installed_store_root(&root, subject)?;
405        let objects = record.columns(subject)?;
406        let exact_registration_ref = encode(record.reference(), subject, "registration ref")?;
407        let exact_activation = encode(&activation, subject, "activation authority")?;
408        let installed = tx
409            .query_row(
410                "SELECT registration_hash, registration_bytes, registration_object, \
411                    activation_authority \
412                 FROM store_device_registration_activations WHERE device_id = ?1",
413                [record.device_id()],
414                |row| {
415                    Ok((
416                        row.get::<_, String>(0)?,
417                        row.get::<_, Vec<u8>>(1)?,
418                        row.get::<_, String>(2)?,
419                        row.get::<_, String>(3)?,
420                    ))
421                },
422            )
423            .optional()
424            .map_err(DbError::from)?;
425        let activated = match installed {
426            None => false,
427            Some(existing)
428                if existing
429                    == (
430                        objects.1.clone(),
431                        objects.2.clone(),
432                        exact_registration_ref,
433                        exact_activation,
434                    ) =>
435            {
436                true
437            }
438            Some(_) => {
439                return Err(DbError::Message(
440                    "Owner recovery device already has different exact activation authority".into(),
441                ));
442            }
443        };
444        let existing: Option<LocalDeviceRegistrationJournalRow> = tx
445            .query_row(
446                "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
447                        initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state \
448                 FROM local_store_device_registration WHERE singleton = 1",
449                [],
450                |row| {
451                    Ok((
452                        row.get(0)?,
453                        row.get(1)?,
454                        row.get(2)?,
455                        row.get(3)?,
456                        row.get(4)?,
457                        row.get(5)?,
458                        row.get(6)?,
459                        row.get(7)?,
460                    ))
461                },
462            )
463            .optional()
464            .map_err(DbError::from)?;
465        if !activated {
466            if let Some(existing) = existing.as_ref() {
467                let same_objects = existing.0 == objects.0
468                    && existing.1 == objects.1
469                    && existing.2 == objects.2
470                    && existing.3 == objects.3
471                    && existing.4 == objects.4
472                    && existing.5 == objects.5
473                    && existing.6 == objects.6;
474                if same_objects {
475                    let state: LocalDeviceRegistrationState = serde_json::from_str(&existing.7)
476                        .map_err(|error| {
477                            DbError::context("parse Owner recovery journal state", error)
478                        })?;
479                    if !matches!(
480                        state,
481                        LocalDeviceRegistrationState::Prepared
482                            | LocalDeviceRegistrationState::Created
483                    ) {
484                        return Err(DbError::Message(
485                            "Owner recovery journal claims activation absent from Store authority"
486                                .into(),
487                        ));
488                    }
489                    let published_ack_count: i64 = tx
490                        .query_row("SELECT COUNT(*) FROM published_store_acks", [], |row| {
491                            row.get(0)
492                        })
493                        .map_err(DbError::from)?;
494                    if published_ack_count != 0
495                        || crate::get_protocol_state_on(&tx, LOCAL_DEVICE_ID_STATE_KEY)?.is_some()
496                    {
497                        return Err(DbError::Message(
498                            "unactivated Owner recovery journal has published local authority"
499                                .into(),
500                        ));
501                    }
502                    tx.commit().map_err(DbError::from)?;
503                    return Ok(false);
504                }
505            }
506        }
507        tx.execute("DELETE FROM local_store_device_registration", [])
508            .map_err(DbError::from)?;
509        tx.execute("DELETE FROM published_store_acks", [])
510            .map_err(DbError::from)?;
511        crate::delete_protocol_state_on(&tx, LOCAL_DEVICE_ID_STATE_KEY)?;
512        let state = encode(
513            &if activated {
514                LocalDeviceRegistrationState::Activated {
515                    authority: activation,
516                }
517            } else {
518                LocalDeviceRegistrationState::Prepared
519            },
520            subject,
521            "journal state",
522        )?;
523        tx.execute(
524            "INSERT INTO local_store_device_registration \
525                 (singleton, device_id, registration_hash, registration_bytes, \
526                  prepared_object, initial_ack_ref, initial_ack_bytes, \
527                  initial_ack_prepared, state) \
528                 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
529            rusqlite::params![
530                objects.0, objects.1, objects.2, objects.3, objects.4, objects.5, objects.6, state,
531            ],
532        )
533        .map_err(DbError::from)?;
534        if activated {
535            tx.execute(
536                "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
537                (LOCAL_DEVICE_ID_STATE_KEY, &objects.0),
538            )
539            .map_err(DbError::from)?;
540            let published_ack = record.published_ack_columns(subject)?;
541            tx.execute(
542                "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot) \
543                     VALUES (1, ?1, ?2)",
544                (&published_ack.0, &published_ack.1),
545            )
546            .map_err(DbError::from)?;
547        }
548        tx.commit().map_err(DbError::from)?;
549        Ok(activated)
550    }
551
552    fn read_local_store_device_registration(
553        &mut self,
554        sql: &'static str,
555    ) -> Result<Option<DurableDeviceRegistration>, DbError> {
556        let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
557        self.conn
558            .query_row(sql, [], |row| {
559                Ok((
560                    row.get::<_, String>(0)?,
561                    row.get::<_, String>(1)?,
562                    row.get::<_, Vec<u8>>(2)?,
563                    row.get::<_, String>(3)?,
564                    row.get::<_, String>(4)?,
565                    row.get::<_, Vec<u8>>(5)?,
566                    row.get::<_, String>(6)?,
567                    row.get::<_, String>(7)?,
568                ))
569            })
570            .optional()
571            .map_err(DbError::from)?
572            .map(
573                |(device_id, hash, bytes, prepared, ack_ref, ack_bytes, ack_prepared, state)| {
574                    let device_id = device_id
575                        .parse()
576                        .map_err(|error| DbError::context("local Store device id", error))?;
577                    let prepared: PreparedExactObject =
578                        serde_json::from_str(&prepared).map_err(|error| {
579                            DbError::context(
580                                "local Store device registration prepared object",
581                                error,
582                            )
583                        })?;
584                    let registration = StoreDeviceRegistration::parse_at(
585                        &bytes,
586                        &self
587                            .verified_store_authority
588                            .required_root_authority_on(records)?,
589                        device_id,
590                    )
591                    .map_err(|error| DbError::context("local Store device registration", error))?;
592                    let initial_ack_ref: StoreAckRef =
593                        serde_json::from_str(&ack_ref).map_err(|error| {
594                            DbError::context("local Store initial acknowledgement ref", error)
595                        })?;
596                    let initial_ack_value = StoreAck::parse_at(
597                        &ack_bytes,
598                        &registration.store_root,
599                        &initial_ack_ref,
600                        &registration,
601                    )
602                    .map_err(|error| {
603                        DbError::context("local Store initial acknowledgement", error)
604                    })?;
605                    let initial_ack_prepared: PreparedExactObject =
606                        serde_json::from_str(&ack_prepared).map_err(|error| {
607                            DbError::context("local Store initial ack object", error)
608                        })?;
609                    Ok(DurableDeviceRegistration {
610                        device_id,
611                        registration_hash: hash.parse().map_err(|error| {
612                            DbError::context("local Store device registration hash", error)
613                        })?,
614                        registration_bytes: bytes,
615                        prepared,
616                        initial_ack_ref,
617                        initial_ack: ExactProtocolObject {
618                            value: initial_ack_value,
619                            bytes: ack_bytes,
620                            prepared: initial_ack_prepared,
621                        },
622                        state: serde_json::from_str(&state).map_err(|error| {
623                            DbError::context("local Store registration journal state", error)
624                        })?,
625                    })
626                },
627            )
628            .transpose()
629    }
630
631    pub(super) fn local_store_device_registration(
632        &mut self,
633    ) -> Result<Option<DurableDeviceRegistration>, DbError> {
634        self.read_local_store_device_registration(
635            "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
636                    initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state \
637             FROM local_store_device_registration WHERE singleton = 1",
638        )
639    }
640
641    fn local_registration_state(
642        tx: &rusqlite::Transaction<'_>,
643        record: &LocalRegistrationRecord,
644        subject: &str,
645    ) -> Result<LocalDeviceRegistrationState, DbError> {
646        let expected = record.columns(subject)?;
647        let durable: LocalDeviceRegistrationJournalRow = tx
648            .query_row(
649                "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
650                        initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state \
651                 FROM local_store_device_registration WHERE singleton = 1",
652                [],
653                |row| {
654                    Ok((
655                        row.get(0)?,
656                        row.get(1)?,
657                        row.get(2)?,
658                        row.get(3)?,
659                        row.get(4)?,
660                        row.get(5)?,
661                        row.get(6)?,
662                        row.get(7)?,
663                    ))
664                },
665            )
666            .map_err(DbError::from)?;
667        if durable.0 != expected.0
668            || durable.1 != expected.1
669            || durable.2 != expected.2
670            || durable.3 != expected.3
671            || durable.4 != expected.4
672            || durable.5 != expected.5
673            || durable.6 != expected.6
674        {
675            return Err(DbError::Message(format!(
676                "{subject} differs from its durable exact objects"
677            )));
678        }
679        serde_json::from_str(&durable.7)
680            .map_err(|error| DbError::context(format!("parse {subject} state"), error))
681    }
682
683    fn mark_local_store_device_registration_published(
684        &mut self,
685        record: LocalRegistrationRecord,
686        subject: &str,
687    ) -> Result<(), DbError> {
688        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
689        let state = Self::local_registration_state(&tx, &record, subject)?;
690        match state {
691            LocalDeviceRegistrationState::Prepared => {
692                tx.execute(
693                    "UPDATE local_store_device_registration SET state = ?1 \
694                     WHERE singleton = 1 AND state = ?2",
695                    rusqlite::params![
696                        encode(
697                            &LocalDeviceRegistrationState::RegistrationPublished,
698                            subject,
699                            "published state",
700                        )?,
701                        encode(
702                            &LocalDeviceRegistrationState::Prepared,
703                            subject,
704                            "prepared state",
705                        )?,
706                    ],
707                )
708                .map_err(DbError::from)?;
709            }
710            LocalDeviceRegistrationState::RegistrationPublished
711            | LocalDeviceRegistrationState::Created
712            | LocalDeviceRegistrationState::Activated { .. } => {}
713            LocalDeviceRegistrationState::RegistrationActivated { .. } => {
714                return Err(DbError::Message(
715                    "activated registration cannot pass through unactivated publication"
716                        .to_string(),
717                ));
718            }
719        }
720        tx.commit().map_err(DbError::from)
721    }
722
723    fn mark_local_store_device_ack_published(
724        &mut self,
725        record: LocalRegistrationRecord,
726        subject: &str,
727    ) -> Result<(), DbError> {
728        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
729        let state = Self::local_registration_state(&tx, &record, subject)?;
730        let target = match state {
731            LocalDeviceRegistrationState::Prepared
732            | LocalDeviceRegistrationState::RegistrationPublished => {
733                LocalDeviceRegistrationState::Created
734            }
735            LocalDeviceRegistrationState::RegistrationActivated { ref authority } => {
736                let published_ack = record.published_ack_columns(subject)?;
737                tx.execute(
738                    "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot) \
739                     VALUES (1, ?1, ?2) ON CONFLICT(singleton) DO NOTHING",
740                    (&published_ack.0, &published_ack.1),
741                )
742                .map_err(DbError::from)?;
743                let stored_ack: (String, String) = tx
744                    .query_row(
745                        "SELECT ack_ref, successor_slot FROM published_store_acks \
746                         WHERE singleton = 1",
747                        [],
748                        |row| Ok((row.get(0)?, row.get(1)?)),
749                    )
750                    .map_err(DbError::from)?;
751                if stored_ack != published_ack {
752                    return Err(DbError::Message(
753                        "activated local acknowledgement differs from its exact cloud object"
754                            .to_string(),
755                    ));
756                }
757                crate::set_protocol_state_on(&tx, LOCAL_DEVICE_ID_STATE_KEY, &record.device_id())?;
758                LocalDeviceRegistrationState::Activated {
759                    authority: authority.clone(),
760                }
761            }
762            LocalDeviceRegistrationState::Created
763            | LocalDeviceRegistrationState::Activated { .. } => {
764                tx.commit().map_err(DbError::from)?;
765                return Ok(());
766            }
767        };
768        let current = encode(&state, subject, "current state")?;
769        let updated = tx
770            .execute(
771                "UPDATE local_store_device_registration SET state = ?1 \
772                 WHERE singleton = 1 AND state = ?2",
773                rusqlite::params![encode(&target, subject, "published state")?, current],
774            )
775            .map_err(DbError::from)?;
776        if updated != 1 {
777            return Err(DbError::Message(
778                "local registration journal changed during acknowledgement publication".to_string(),
779            ));
780        }
781        tx.commit().map_err(DbError::from)
782    }
783}
784
785impl StoreDatabase {
786    pub async fn stage_local_store_device_registration(
787        &self,
788        registration: ExactProtocolObject<StoreDeviceRegistration>,
789        initial_ack_ref: StoreAckRef,
790        initial_ack: ExactProtocolObject<StoreAck>,
791    ) -> Result<(), DbError> {
792        const SUBJECT: &str = "local registration staging graph";
793        let record =
794            LocalRegistrationRecord::checked(registration, initial_ack_ref, initial_ack, SUBJECT)?;
795        self.call_store(move |session| {
796            session.stage_local_store_device_registration(
797                record,
798                LocalDeviceRegistrationState::Prepared,
799                SUBJECT,
800            )
801        })
802        .await
803    }
804
805    pub async fn stage_activated_local_store_device_registration(
806        &self,
807        registration: ExactProtocolObject<StoreDeviceRegistration>,
808        initial_ack_ref: StoreAckRef,
809        initial_ack: ExactProtocolObject<StoreAck>,
810        authority: coven_protocol::store_commit::StoreDeviceRegistrationActivation,
811    ) -> Result<(), DbError> {
812        const SUBJECT: &str = "activated local registration staging graph";
813        let record =
814            LocalRegistrationRecord::checked(registration, initial_ack_ref, initial_ack, SUBJECT)?;
815        self.call_store(move |session| {
816            session.stage_activated_local_store_device_registration(record, authority, SUBJECT)
817        })
818        .await
819    }
820
821    pub async fn install_existing_local_founder_device(
822        &self,
823        registration: ExactProtocolObject<StoreDeviceRegistration>,
824        initial_ack_ref: StoreAckRef,
825        initial_ack: ExactProtocolObject<StoreAck>,
826    ) -> Result<(), DbError> {
827        const SUBJECT: &str = "existing founder device graph";
828        let record = LocalRegistrationRecord::checked_at_stream_start(
829            registration,
830            initial_ack_ref,
831            initial_ack,
832            SUBJECT,
833        )?;
834        self.call_store(move |session| {
835            session.install_existing_local_founder_device(record, SUBJECT)
836        })
837        .await
838    }
839
840    pub async fn stage_owner_recovery_registration(
841        &self,
842        registration: ExactProtocolObject<StoreDeviceRegistration>,
843        initial_ack_ref: StoreAckRef,
844        initial_ack: ExactProtocolObject<StoreAck>,
845        activation: coven_protocol::store_commit::StoreDeviceRegistrationActivation,
846    ) -> Result<bool, DbError> {
847        let (
848            coven_protocol::store_commit::StoreDeviceRegistrationOrigin::Recovery {
849                recovery_id: origin_recovery_id,
850                recovery_slot,
851                owner_grant,
852                ..
853            },
854            coven_protocol::store_commit::StoreDeviceRegistrationActivation::Recovery {
855                recovery_id: activation_recovery_id,
856                node,
857            },
858        ) = (&registration.value.origin, &activation)
859        else {
860            return Err(DbError::Message(
861                "Owner recovery journal requires one Recovery registration authority".into(),
862            ));
863        };
864        if origin_recovery_id != activation_recovery_id
865            || node.object.slot() != recovery_slot
866            || node.owner_grant != *owner_grant
867        {
868            return Err(DbError::Message(
869                "Owner recovery registration differs from its activation authority".into(),
870            ));
871        }
872        const SUBJECT: &str = "Owner recovery registration graph";
873        let record = LocalRegistrationRecord::checked_at_stream_start(
874            registration,
875            initial_ack_ref,
876            initial_ack,
877            SUBJECT,
878        )?;
879        self.call_store(move |session| {
880            session.stage_owner_recovery_registration(record, activation, SUBJECT)
881        })
882        .await
883    }
884
885    pub async fn oldest_unpublished_store_device_registration(
886        &self,
887    ) -> Result<Option<DurableDeviceRegistration>, DbError> {
888        let registration = self
889            .read_local_store_device_registration(
890                "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
891                    initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state \
892             FROM local_store_device_registration WHERE singleton = 1",
893            )
894            .await?;
895        Ok(registration.filter(|registration| {
896            matches!(
897                registration.state,
898                LocalDeviceRegistrationState::Prepared
899                    | LocalDeviceRegistrationState::RegistrationPublished
900                    | LocalDeviceRegistrationState::RegistrationActivated { .. }
901            )
902        }))
903    }
904
905    pub async fn read_local_store_device_registration(
906        &self,
907        sql: &'static str,
908    ) -> Result<Option<DurableDeviceRegistration>, DbError> {
909        self.call_store(move |session| session.read_local_store_device_registration(sql))
910            .await
911    }
912
913    pub async fn mark_local_store_device_registration_published(
914        &self,
915        registration: ExactProtocolObject<StoreDeviceRegistration>,
916        initial_ack_ref: StoreAckRef,
917        initial_ack_object: ExactProtocolObject<StoreAck>,
918    ) -> Result<(), DbError> {
919        const SUBJECT: &str = "published local registration graph";
920        let record = LocalRegistrationRecord::checked(
921            registration,
922            initial_ack_ref,
923            initial_ack_object,
924            SUBJECT,
925        )?;
926        self.call_store(move |session| {
927            session.mark_local_store_device_registration_published(record, SUBJECT)
928        })
929        .await
930    }
931
932    pub async fn mark_local_store_device_ack_published(
933        &self,
934        registration: ExactProtocolObject<StoreDeviceRegistration>,
935        initial_ack_ref: StoreAckRef,
936        initial_ack_object: ExactProtocolObject<StoreAck>,
937    ) -> Result<(), DbError> {
938        const SUBJECT: &str = "published local acknowledgement graph";
939        let record = LocalRegistrationRecord::checked(
940            registration,
941            initial_ack_ref,
942            initial_ack_object,
943            SUBJECT,
944        )?;
945        self.call_store(move |session| {
946            session.mark_local_store_device_ack_published(record, SUBJECT)
947        })
948        .await
949    }
950}