Skip to main content

coven_database/store/store_session/
owner_recovery_publication.rs

1use super::*;
2use crate::*;
3use coven_protocol::store_commit::{
4    StoreBatchCommit, StoreCommitCoord, StoreDeviceRegistration,
5    StoreDeviceRegistrationActivationRef, StoreDeviceRegistrationOrigin, VerifiedStoreBatchCommit,
6};
7use rusqlite::OptionalExtension;
8
9#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10#[serde(deny_unknown_fields)]
11struct DurableOwnerRecoveryPublication {
12    commit: DurablePreparedProtocolObject,
13    head: DurablePreparedProtocolObject,
14    history_evidence: coven_protocol::store_commit::RetainedMergeCommitEvidence,
15}
16
17pub(super) fn complete_owner_recovery_publication_on(
18    transaction: &rusqlite::Transaction<'_>,
19    commit: &VerifiedStoreBatchCommit,
20    head: &coven_protocol::store_commit::StoreDeviceHead,
21    head_object: &coven_protocol::objects::ExactObjectRef,
22) -> Result<(), DbError> {
23    if complete_matching_owner_recovery_publication_on(transaction, commit, head, head_object)? {
24        return Ok(());
25    }
26    Err(DbError::Message(
27        "completed Owner recovery has no exact publication journal".into(),
28    ))
29}
30
31pub(super) fn complete_matching_owner_recovery_publication_on(
32    transaction: &rusqlite::Transaction<'_>,
33    commit: &VerifiedStoreBatchCommit,
34    head: &coven_protocol::store_commit::StoreDeviceHead,
35    head_object: &coven_protocol::objects::ExactObjectRef,
36) -> Result<bool, DbError> {
37    let stored: Option<(String, String)> = transaction
38        .query_row(
39            "SELECT registration_hash, publication
40             FROM local_owner_recovery_publication WHERE singleton = 1",
41            [],
42            |row| Ok((row.get(0)?, row.get(1)?)),
43        )
44        .optional()
45        .map_err(DbError::from)?;
46    let Some(stored) = stored else {
47        return Ok(false);
48    };
49    if stored.0 != commit.author_registration.registration_hash.to_string() {
50        return Ok(false);
51    }
52    let durable: DurableOwnerRecoveryPublication = serde_json::from_str(&stored.1)
53        .map_err(|error| DbError::context("parse completed Owner recovery publication", error))?;
54    if durable.commit.semantic_bytes() != commit.value().to_bytes()
55        || durable.commit.prepared().reference() != &commit.reference().object
56        || durable.head.semantic_bytes() != head.to_bytes()
57        || durable.head.prepared().reference() != head_object
58    {
59        return Err(DbError::Message(
60            "completed Owner recovery differs from its exact publication journal".into(),
61        ));
62    }
63    let deleted = transaction
64        .execute(
65            "DELETE FROM local_owner_recovery_publication
66             WHERE singleton = 1 AND registration_hash = ?1 AND publication = ?2",
67            (&stored.0, &stored.1),
68        )
69        .map_err(DbError::from)?;
70    if deleted != 1 {
71        return Err(DbError::Message(
72            "Owner recovery publication changed during completion".into(),
73        ));
74    }
75    Ok(true)
76}
77
78impl DurableOwnerRecoveryPublication {
79    fn from_publication(publication: OwnerRecoveryPublication) -> Result<Self, DbError> {
80        if publication.commit.bytes != publication.commit.value.value().to_bytes()
81            || publication.head.bytes != publication.head.value.to_bytes()
82        {
83            return Err(DbError::Message(
84                "Owner recovery publication carries noncanonical semantic bytes".into(),
85            ));
86        }
87        Ok(Self {
88            commit: DurablePreparedProtocolObject::new(
89                publication.commit.bytes,
90                publication.commit.prepared,
91            ),
92            head: DurablePreparedProtocolObject::new(
93                publication.head.bytes,
94                publication.head.prepared,
95            ),
96            history_evidence: publication.history_evidence,
97        })
98    }
99}
100
101impl StoreSession<'_> {
102    fn verify_owner_recovery_publication(
103        &mut self,
104        durable: DurableOwnerRecoveryPublication,
105    ) -> Result<(OwnerRecoveryPublication, ObjectHash), DbError> {
106        let local = self.local_store_device_registration()?.ok_or_else(|| {
107            DbError::Message("Owner recovery registration journal is absent".into())
108        })?;
109        if local.state != LocalDeviceRegistrationState::Created {
110            return Err(DbError::Message(
111                "Owner recovery publication requires created registration objects".into(),
112            ));
113        }
114        let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
115        let root = self
116            .verified_store_authority
117            .required_root_authority_on(records)?;
118        let registration =
119            StoreDeviceRegistration::parse_at(&local.registration_bytes, &root, local.device_id)
120                .map_err(|error| DbError::context("Owner recovery local registration", error))?;
121        let registration_ref =
122            coven_protocol::store_commit::StoreDeviceRegistrationRef::from_registration(
123                &registration,
124                local.prepared.reference().clone(),
125            );
126        if registration_ref.registration_hash != local.registration_hash {
127            return Err(DbError::Message(
128                "Owner recovery local registration hash differs from its exact reference".into(),
129            ));
130        }
131        let StoreDeviceRegistrationOrigin::Recovery {
132            recovery_id,
133            recovery_slot,
134            owner_grant,
135        } = &registration.origin
136        else {
137            return Err(DbError::Message(
138                "Owner recovery publication has a non-recovery registration".into(),
139            ));
140        };
141
142        durable
143            .commit
144            .prepared()
145            .reference()
146            .verify(durable.commit.prepared().stored_bytes())
147            .map_err(|error| DbError::context("Owner recovery exact commit", error))?;
148        let decoded: StoreBatchCommit = serde_json::from_slice(durable.commit.semantic_bytes())
149            .map_err(|error| DbError::context("Owner recovery commit", error))?;
150        let stream_id = coven_protocol::store_commit::StreamActivation::device_authorized_stream_id(
151            root.store_root_hash,
152            &registration_ref,
153            coven_protocol::store_commit::StreamAnchorDomain::StoreAnnouncements,
154        );
155        let coord = StoreCommitCoord {
156            stream_id,
157            sequence: decoded.seq(),
158        };
159        let commit = VerifiedStoreBatchCommit::parse_prepared(
160            durable.commit.semantic_bytes(),
161            root.store_root_hash,
162            coord,
163            durable.commit.prepared().reference().clone(),
164            &registration,
165        )
166        .map_err(|error| DbError::context("verify Owner recovery commit", error))?;
167        let [activation] = commit.device_registrations() else {
168            return Err(DbError::Message(
169                "Owner recovery commit must carry exactly one registration activation".into(),
170            ));
171        };
172        let StoreDeviceRegistrationActivationRef::Recovery {
173            recovery_id: activation_recovery_id,
174            node,
175        } = &activation.authority
176        else {
177            return Err(DbError::Message(
178                "Owner recovery commit carries another registration authority".into(),
179            ));
180        };
181        if commit.seq() != 1
182            || commit.author_registration != registration_ref
183            || activation.registration != registration_ref
184            || activation_recovery_id != recovery_id
185            || node.object.slot() != recovery_slot
186            || &node.owner_grant != owner_grant
187            || commit.value().to_bytes() != durable.commit.semantic_bytes()
188        {
189            return Err(DbError::Message(
190                "Owner recovery commit differs from its local recovery authority".into(),
191            ));
192        }
193        durable
194            .history_evidence
195            .validate_for(commit.reference(), commit.value())
196            .map_err(|error| DbError::context("Owner recovery history evidence", error))?;
197
198        durable
199            .head
200            .prepared()
201            .reference()
202            .verify(durable.head.prepared().stored_bytes())
203            .map_err(|error| DbError::context("Owner recovery exact head", error))?;
204        let head = coven_protocol::store_commit::StoreDeviceHead::parse_at(
205            durable.head.semantic_bytes(),
206            root.store_root_hash,
207            &registration,
208            commit.reference(),
209        )
210        .map_err(|error| DbError::context("verify Owner recovery head", error))?;
211        let coven_protocol::store_commit::DeviceStreamAnchor::StoreAnnouncements { first_slot } =
212            &registration.store_commits
213        else {
214            return Err(DbError::Message(
215                "Owner recovery registration has no announcement stream anchor".into(),
216            ));
217        };
218        let activation = registration
219            .store_announcement_activation(&registration_ref)
220            .map_err(|error| DbError::context("Owner recovery announcement activation", error))?
221            .activation_id();
222        if durable.head.prepared().reference().slot() != first_slot
223            || head.successor.predecessor.is_some()
224            || head.successor.activation != activation
225            || &head.successor.next_slot == first_slot
226            || head.to_bytes() != durable.head.semantic_bytes()
227        {
228            return Err(DbError::Message(
229                "Owner recovery head differs from its first announcement position".into(),
230            ));
231        }
232
233        Ok((
234            OwnerRecoveryPublication {
235                commit: ExactProtocolObject {
236                    value: commit,
237                    bytes: durable.commit.semantic_bytes,
238                    prepared: durable.commit.prepared,
239                },
240                head: ExactProtocolObject {
241                    value: head,
242                    bytes: durable.head.semantic_bytes,
243                    prepared: durable.head.prepared,
244                },
245                history_evidence: durable.history_evidence,
246            },
247            local.registration_hash,
248        ))
249    }
250
251    fn stage_owner_recovery_publication(
252        &mut self,
253        publication: OwnerRecoveryPublication,
254    ) -> Result<OwnerRecoveryPublication, DbError> {
255        let durable = DurableOwnerRecoveryPublication::from_publication(publication)?;
256        let (verified, registration_hash) =
257            self.verify_owner_recovery_publication(durable.clone())?;
258        let registration_hash = registration_hash.to_string();
259        let encoded = serde_json::to_string(&durable)
260            .map_err(|error| DbError::context("serialize Owner recovery publication", error))?;
261        crate::store::store_session::StoreRecords::new(self.conn, self.store_dir)
262            .stage_owner_recovery_publication(&registration_hash, &encoded)?;
263        Ok(verified)
264    }
265
266    fn owner_recovery_publication(&mut self) -> Result<Option<OwnerRecoveryPublication>, DbError> {
267        let stored = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir)
268            .owner_recovery_publication_row()?;
269        stored
270            .map(|(registration_hash, encoded)| {
271                let durable = serde_json::from_str(&encoded)
272                    .map_err(|error| DbError::context("parse Owner recovery publication", error))?;
273                let (publication, local_registration_hash) =
274                    self.verify_owner_recovery_publication(durable)?;
275                if registration_hash != local_registration_hash.to_string() {
276                    return Err(DbError::Message(
277                        "Owner recovery publication belongs to another local registration".into(),
278                    ));
279                }
280                Ok(publication)
281            })
282            .transpose()
283    }
284}
285
286impl StoreDatabase {
287    pub async fn stage_owner_recovery_publication(
288        &self,
289        publication: OwnerRecoveryPublication,
290    ) -> Result<OwnerRecoveryPublication, DbError> {
291        self.call_store(move |session| session.stage_owner_recovery_publication(publication))
292            .await
293    }
294
295    pub async fn owner_recovery_publication(
296        &self,
297    ) -> Result<Option<OwnerRecoveryPublication>, DbError> {
298        self.call_store(|session| session.owner_recovery_publication())
299            .await
300    }
301}