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