Skip to main content

coven_database/store/store_session/
preparation.rs

1use super::{
2    publication_state::{
3        MergeAbandonmentOutcome, MergeCandidateAbandonmentPreparation, PreparedStoreWriteState,
4        StoreWritePreparation,
5    },
6    StoreDatabase, StoreSession,
7};
8use crate::{
9    persist_exact_remote_object_on, DbError, DurablePreparedProtocolObject, StoreWriteBase,
10    LOCAL_DEVICE_ID_STATE_KEY,
11};
12use coven_protocol::remote_object::RemoteObjectRecord;
13use coven_protocol::store_commit::{
14    CommitFrontier, StoreCommitCoord, StoreDeviceHead, StoreDeviceRegistrationRef,
15};
16use coven_protocol::write::WriteStatus;
17use rusqlite::OptionalExtension;
18
19impl StoreSession<'_> {
20    fn table_schema_for_apply(&mut self) -> Result<crate::TableSchema, DbError> {
21        crate::TableSchema::for_apply(self.conn, self.synced_tables, self.gates)
22    }
23
24    fn prepare_store_write_commit(&mut self, stage: StoreWritePreparation) -> Result<(), DbError> {
25        let author = self.activated_registration(&stage.commit.value.author_registration)?;
26        if author.value().store_root != stage.root {
27            return Err(DbError::Message(
28                "prepared Store write belongs to another verified Store root".to_string(),
29            ));
30        }
31        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
32        let local_device_id = crate::required_protocol_state_on(&tx, LOCAL_DEVICE_ID_STATE_KEY)?;
33        let registration_object: String = tx
34            .query_row(
35                "SELECT registration_object \
36                 FROM store_device_registration_activations WHERE device_id = ?1",
37                [&local_device_id],
38                |row| row.get(0),
39            )
40            .map_err(DbError::from)?;
41        let registration_ref: StoreDeviceRegistrationRef =
42            serde_json::from_str(&registration_object).map_err(|error| {
43                DbError::context("prepared write exact registration ref", error)
44            })?;
45        if registration_ref != stage.commit.value.author_registration
46            || registration_ref != stage.head.value.author_registration
47        {
48            return Err(DbError::Message(
49                "prepared Store commit/head author registration differs from local activation"
50                    .to_string(),
51            ));
52        }
53        let registration = stage.commit.value.author();
54        if author.value() != registration {
55            return Err(DbError::Message(
56                "prepared write author registration differs from its activated bytes".to_string(),
57            ));
58        }
59        let stream_id = coven_protocol::store_commit::StreamActivation::device_authorized_stream_id(
60            stage.root.store_root_hash,
61            &registration_ref,
62            coven_protocol::store_commit::StreamAnchorDomain::StoreAnnouncements,
63        );
64        let expected_coord = StoreCommitCoord {
65            stream_id,
66            sequence: stage.commit.value.seq(),
67        };
68        if stage.commit.value.store_root_hash() != stage.root.store_root_hash
69            || stage.commit.value.reference().coord != expected_coord
70            || stage.commit.value.reference().object != *stage.commit.prepared.reference()
71        {
72            return Err(DbError::Message(
73                "authenticated prepared Store commit differs from its current local authority"
74                    .to_string(),
75            ));
76        }
77        if stage.commit.value.write_id != stage.write_id {
78            return Err(DbError::Message(
79                "prepared write id differs from signed commit".to_string(),
80            ));
81        }
82        let commit_ref = stage.commit.value.reference().clone();
83        if stage.head.value.commit != commit_ref {
84            return Err(DbError::Message(
85                "prepared Store head does not activate the exact prepared commit".to_string(),
86            ));
87        }
88        stage
89            .history_evidence
90            .validate_for(&commit_ref, stage.commit.value.value())
91            .map_err(|error| DbError::context("prepared Store history evidence", error))?;
92        StoreDeviceHead::parse_at(
93            &stage.head.value.to_bytes(),
94            stage.root.store_root_hash,
95            registration,
96            &commit_ref,
97        )
98        .map_err(|error| DbError::context("verify prepared Store head", error))?;
99        let (stored_base, stored_status, stored_preparation): (
100            Option<String>,
101            String,
102            Option<String>,
103        ) = tx
104            .query_row(
105                "SELECT base, status, prepared
106                 FROM store_writes WHERE write_id = ?1",
107                [stage.write_id.as_str()],
108                |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
109            )
110            .map_err(DbError::from)?;
111        if stored_status != "\"pending\"" || stored_preparation.is_some() {
112            return Err(DbError::Message(format!(
113                "write {} is not an unprepared pending write",
114                stage.write_id
115            )));
116        }
117        let stored_base = stored_base.ok_or_else(|| {
118            DbError::Message(format!(
119                "pending write {} carries no commit base",
120                stage.write_id
121            ))
122        })?;
123        let partitions = crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
124            .store_write_partitions(stage.write_id.as_str())?;
125        let stored_base: StoreWriteBase = serde_json::from_str(&stored_base)
126            .map_err(|error| DbError::context(format!("write {} base", stage.write_id), error))?;
127        let mut stored_dependencies = CommitFrontier::from_refs(stored_base.dependencies)
128            .map_err(|error| DbError::context("stored write dependencies", error))?;
129        let observed_predecessor = stored_dependencies.0.remove(&stream_id);
130        if stored_dependencies.commits() != stage.commit.value.merge_dependencies() {
131            return Err(DbError::Message(format!(
132                "prepared commit dependencies differ from write {}",
133                stage.write_id
134            )));
135        }
136        let another_prepared: Option<String> = tx
137            .query_row(
138                "SELECT write_id FROM store_writes
139                 WHERE prepared IS NOT NULL AND write_id != ?1
140                 ORDER BY ordinal LIMIT 1",
141                [stage.write_id.as_str()],
142                |row| row.get(0),
143            )
144            .optional()
145            .map_err(DbError::from)?;
146        if let Some(other_write_id) = another_prepared {
147            return Err(DbError::Message(format!(
148                "write {other_write_id} already owns Store publication"
149            )));
150        }
151        let durable_predecessor =
152            crate::store::materialized_commit_index::latest_position_for_device_on(
153                &tx,
154                &stream_id.to_string(),
155            )?;
156        if observed_predecessor.as_ref().is_some_and(|observed| {
157            durable_predecessor.as_ref().is_none_or(|current| {
158                current.coord.sequence() < observed.coord.sequence()
159                    || current.coord.sequence() == observed.coord.sequence() && current != observed
160            })
161        }) {
162            return Err(DbError::Message(format!(
163                "outbound Store commit predecessor does not cover write {} capture frontier",
164                stage.write_id
165            )));
166        }
167        let expected_seq = durable_predecessor
168            .as_ref()
169            .map_or(1, |reference| reference.coord.sequence().saturating_add(1));
170        if stage.commit.value.seq() != expected_seq
171            || stage.commit.value.order.predecessor() != durable_predecessor.as_ref()
172        {
173            return Err(DbError::Message(format!(
174                "outbound Store commit exact predecessor differs from durable {durable_predecessor:?}"
175            )));
176        }
177
178        let mut object_ids = std::collections::BTreeSet::new();
179        for remote in &stage.remote_objects {
180            remote
181                .validate()
182                .map_err(|error| DbError::context("prepared remote object", error))?;
183            if !object_ids.insert(remote.object_id()) {
184                return Err(DbError::Message(
185                    "prepared write contains a duplicate remote object".to_string(),
186                ));
187            }
188        }
189        crate::validate_prepared_audience_blob_graph(&object_ids, &stage.audiences)?;
190        for remote in &stage.remote_objects {
191            crate::persist_prepared_remote_object_on(
192                &tx,
193                self.store_dir,
194                remote,
195                &commit_ref,
196                "candidate audience object",
197            )?;
198        }
199        let commit_remote = RemoteObjectRecord::candidate_commit(
200            commit_ref.clone(),
201            &stage.commit.value.to_bytes(),
202            stage.commit.prepared.stored_bytes(),
203        )
204        .map_err(|error| DbError::context("prepared candidate commit", error))?;
205        persist_exact_remote_object_on(&tx, self.store_dir, &commit_remote, "candidate commit")?;
206        let expected_partition_count = usize::from(partitions.store.is_some())
207            .checked_add(partitions.circles.len())
208            .ok_or_else(|| DbError::Message("audience partition count overflow".to_string()))?;
209        if stage.audiences.packages.len() != expected_partition_count {
210            return Err(DbError::Message(
211                "prepared audience packages do not cover every write partition".to_string(),
212            ));
213        }
214        let mut indexed = std::collections::BTreeSet::new();
215        for package in &stage.audiences.packages {
216            let value = package.package();
217            if value.store_root_hash() != stage.root.store_root_hash
218                || value.write_id() != &stage.write_id
219                || value.commit_coord() != &commit_ref.coord
220                || value.candidate_family() != stage.commit.value.candidate_family()
221            {
222                return Err(DbError::Message(
223                    "prepared audience package differs from its exact Store commit".to_string(),
224                ));
225            }
226            match value.audience() {
227                coven_protocol::audience_package::PackageAudience::Store => {
228                    let partition = partitions.store.as_ref().ok_or_else(|| {
229                        DbError::Message(
230                            "prepared Store package has no Store partition".to_string(),
231                        )
232                    })?;
233                    if value.changeset() != partition.changeset {
234                        return Err(DbError::Message(
235                            "prepared Store package changeset differs from its partition"
236                                .to_string(),
237                        ));
238                    }
239                    stage
240                        .commit
241                        .value
242                        .verify_store_package(package.semantic_bytes())
243                        .map_err(DbError::from)?;
244                }
245                coven_protocol::audience_package::PackageAudience::Circle { circle_id, .. } => {
246                    let partition = partitions
247                        .circles
248                        .iter()
249                        .find(|partition| {
250                            partition.audience
251                                == coven_protocol::circle::Audience::Circle(*circle_id)
252                        })
253                        .ok_or_else(|| {
254                            DbError::Message(format!(
255                                "prepared Circle package {circle_id} has no partition"
256                            ))
257                        })?;
258                    if value.changeset() != partition.changeset {
259                        return Err(DbError::Message(format!(
260                            "prepared Circle package {circle_id} changeset differs from its partition"
261                        )));
262                    }
263                    stage
264                        .commit
265                        .value
266                        .verify_circle_package(*circle_id, package.semantic_bytes())
267                        .map_err(DbError::from)?;
268                }
269            }
270            indexed.insert(package.remote_object_id());
271        }
272        indexed.extend(
273            stage
274                .audiences
275                .blobs
276                .iter()
277                .map(crate::PreparedAudienceBlob::remote_object_id),
278        );
279        debug_assert_eq!(indexed, object_ids);
280        super::prepared_remote_objects::persist_prepared_audience_objects_on(
281            &tx,
282            self.store_dir,
283            &stage.write_id,
284            &stage.audiences.packages,
285            &stage.audiences.blobs,
286        )?;
287        let head_ref = coven_protocol::store_commit::StoreDeviceHeadRef {
288            head_hash: stage.head.value.head_hash(),
289            object: stage.head.prepared.reference().clone(),
290        };
291        let head_remote = RemoteObjectRecord::candidate_activated_store_head(
292            head_ref,
293            &stage.head.value.to_bytes(),
294            stage.head.prepared.stored_bytes(),
295            commit_ref.clone(),
296        )
297        .map_err(|error| DbError::context("prepared Store head", error))?;
298        persist_exact_remote_object_on(&tx, self.store_dir, &head_remote, "Store head")?;
299
300        let prepared = PreparedStoreWriteState::Publication {
301            commit: DurablePreparedProtocolObject::new(
302                stage.commit.value.to_bytes(),
303                stage.commit.prepared,
304            ),
305            head: DurablePreparedProtocolObject::new(
306                stage.head.value.to_bytes(),
307                stage.head.prepared,
308            ),
309            history_evidence: stage.history_evidence,
310            local_cleanup: stage.local_cleanup,
311            completion: stage.completion,
312        };
313        let prepared = serde_json::to_string(&prepared)
314            .map_err(|error| DbError::context("serialize prepared Store write", error))?;
315        let status = serde_json::to_string(&WriteStatus::Publishing)
316            .map_err(|error| DbError::context("serialize write status", error))?;
317        let updated = tx
318            .execute(
319                "UPDATE store_writes SET prepared = ?2, status = ?3
320                 WHERE write_id = ?1 AND prepared IS NULL AND status = '\"pending\"'",
321                rusqlite::params![stage.write_id.as_str(), prepared, status],
322            )
323            .map_err(DbError::from)?;
324        if updated != 1 {
325            return Err(DbError::Message(format!(
326                "write {} lost pending preparation ownership",
327                stage.write_id
328            )));
329        }
330        tx.commit().map_err(DbError::from)?;
331        Ok(())
332    }
333
334    fn prepare_merge_candidate_abandonment(
335        &mut self,
336        stage: MergeCandidateAbandonmentPreparation,
337    ) -> Result<(), DbError> {
338        let verified_authority = &mut *self.verified_store_authority;
339        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
340        let (raw_status, raw_prepared): (String, String) = tx
341            .query_row(
342                "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
343                [stage.write_id.as_str()],
344                |row| Ok((row.get(0)?, row.get(1)?)),
345            )
346            .map_err(DbError::from)?;
347        let status: WriteStatus = serde_json::from_str(&raw_status)
348            .map_err(|error| DbError::context("Merge abandonment status", error))?;
349        if !matches!(status, WriteStatus::Blocked(_)) {
350            return Err(DbError::Message(format!(
351                "write {} is not blocked",
352                stage.write_id
353            )));
354        }
355        let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
356            .map_err(|error| DbError::context("prepared Merge candidate", error))?;
357        let PreparedStoreWriteState::Publication {
358            commit: candidate_commit,
359            head: candidate_head,
360            history_evidence: candidate_history_evidence,
361            local_cleanup,
362            completion,
363        } = prepared
364        else {
365            return Err(DbError::Message(
366                "Merge abandonment requires one prepared candidate".to_string(),
367            ));
368        };
369        let store_transaction =
370            crate::store::store_session::StoreTransaction::new(&tx, self.store_dir);
371        let candidate = store_transaction.prepared_merge_candidate_parts(
372            verified_authority,
373            candidate_commit.semantic_bytes(),
374            candidate_commit.prepared().reference(),
375            candidate_head.semantic_bytes(),
376            candidate_head.prepared().reference(),
377        )?;
378        if candidate.commit.write_id != stage.write_id {
379            return Err(DbError::Message(
380                "prepared Merge candidate differs from its write identity".to_string(),
381            ));
382        }
383        let root = store_transaction.required_root_authority(verified_authority)?;
384        let registration = store_transaction.activated_registration(
385            verified_authority,
386            &root,
387            &candidate.commit.author_registration,
388        )?;
389        if stage.commit.value.store_root_hash() != root.store_root_hash
390            || stage.commit.value.author() != &registration
391            || stage.commit.value.reference().coord != candidate.reference.coord
392            || stage.commit.value.reference().object != *stage.commit.prepared.reference()
393        {
394            return Err(DbError::Message(
395                "authenticated Merge abandonment commit differs from its current local authority"
396                    .to_string(),
397            ));
398        }
399        if stage.commit.value.write_id != stage.write_id
400            || stage.commit.value.abandoned_candidates()
401                != [coven_protocol::store_commit::CandidateCleanupManifest {
402                    candidate: coven_protocol::store_commit::StoreBatchCommitDeletionTarget {
403                        coord: candidate.reference.coord.clone(),
404                        object: candidate.reference.object.clone(),
405                        canonical_signed_bytes: candidate.canonical_signed_bytes.clone(),
406                    },
407                }]
408        {
409            return Err(DbError::Message(
410                "Merge abandonment does not name its exact prepared candidate".to_string(),
411            ));
412        }
413        let authority_ref = stage.commit.value.reference().clone();
414        if stage.head.value.commit != authority_ref
415            || stage.head.prepared.reference().slot() != candidate.head_object.slot()
416            || stage.head.value.successor != candidate.head.successor
417            || stage.head.value.author_registration != candidate.commit.author_registration
418        {
419            return Err(DbError::Message(
420                "Merge abandonment head differs from the candidate competition point".to_string(),
421            ));
422        }
423        stage
424            .history_evidence
425            .validate_for(&authority_ref, stage.commit.value.value())
426            .map_err(|error| DbError::context("Merge abandonment history evidence", error))?;
427        StoreDeviceHead::parse_at(
428            &stage.head.value.to_bytes(),
429            root.store_root_hash,
430            &registration,
431            &authority_ref,
432        )
433        .map_err(|error| DbError::context("verify Merge abandonment head", error))?;
434        let authority_commit = RemoteObjectRecord::candidate_commit(
435            authority_ref.clone(),
436            &stage.commit.value.to_bytes(),
437            stage.commit.prepared.stored_bytes(),
438        )
439        .map_err(|error| DbError::context("Merge abandonment commit", error))?;
440        persist_exact_remote_object_on(
441            &tx,
442            self.store_dir,
443            &authority_commit,
444            "Merge abandonment commit",
445        )?;
446        let authority_head_ref = coven_protocol::store_commit::StoreDeviceHeadRef {
447            head_hash: stage.head.value.head_hash(),
448            object: stage.head.prepared.reference().clone(),
449        };
450        let authority_head = RemoteObjectRecord::candidate_activated_store_head(
451            authority_head_ref,
452            &stage.head.value.to_bytes(),
453            stage.head.prepared.stored_bytes(),
454            authority_ref,
455        )
456        .map_err(|error| DbError::context("Merge abandonment head", error))?;
457        persist_exact_remote_object_on(
458            &tx,
459            self.store_dir,
460            &authority_head,
461            "Merge abandonment head",
462        )?;
463        let replacement = PreparedStoreWriteState::MergeAbandonment {
464            candidate_commit,
465            candidate_head,
466            candidate_history_evidence,
467            authority_commit: DurablePreparedProtocolObject::new(
468                stage.commit.value.to_bytes(),
469                stage.commit.prepared,
470            ),
471            authority_head: DurablePreparedProtocolObject::new(
472                stage.head.value.to_bytes(),
473                stage.head.prepared,
474            ),
475            authority_history_evidence: stage.history_evidence,
476            outcome: MergeAbandonmentOutcome::Prepared,
477            local_cleanup,
478            completion,
479        };
480        let replacement = serde_json::to_string(&replacement)
481            .map_err(|error| DbError::context("serialize Merge abandonment", error))?;
482        let publishing = serde_json::to_string(&WriteStatus::Publishing)
483            .map_err(|error| DbError::context("serialize Merge abandonment status", error))?;
484        let updated = tx
485            .execute(
486                "UPDATE store_writes SET prepared = ?2, status = ?3
487                 WHERE write_id = ?1 AND prepared = ?4
488                   AND json_extract(status, '$.blocked') IS NOT NULL",
489                rusqlite::params![
490                    stage.write_id.as_str(),
491                    replacement,
492                    publishing,
493                    raw_prepared
494                ],
495            )
496            .map_err(DbError::from)?;
497        if updated != 1 {
498            return Err(DbError::Message(
499                "blocked Merge candidate changed during abandonment preparation".to_string(),
500            ));
501        }
502        tx.commit().map_err(DbError::from)?;
503        Ok(())
504    }
505
506    #[cfg(any(test, feature = "test-utils"))]
507    fn enqueue_store_changeset_for_test(
508        &mut self,
509        write_id: coven_protocol::write::WriteId,
510        changeset: Vec<u8>,
511    ) -> Result<(), DbError> {
512        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
513        let base = StoreWriteBase {
514            dependencies: crate::store::materialized_commit_index::materialized_frontier_on(
515                &tx, None,
516            )?,
517        };
518        let partitions = vec![crate::AudiencePartition {
519            audience: coven_protocol::circle::Audience::Store,
520            control: None,
521            changeset: changeset.clone(),
522        }];
523        let blob_facts = super::host_write_capture::capture_partition_blob_facts_on(
524            &tx,
525            &partitions,
526            self.blob_decls,
527        )?;
528        let changeset_hash =
529            crate::payload_store::write_payload_blocking(&tx, self.store_dir, &changeset)?;
530        crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
531            .insert_store_write(&write_id, &partitions, changeset_hash, &base, &blob_facts)?;
532        tx.commit().map_err(DbError::from)
533    }
534}
535
536impl StoreDatabase {
537    pub async fn table_schema_for_apply(&self) -> Result<crate::TableSchema, DbError> {
538        self.call_store(|session| session.table_schema_for_apply())
539            .await
540    }
541
542    pub async fn prepare_store_write_commit(
543        &self,
544        stage: StoreWritePreparation,
545    ) -> Result<(), DbError> {
546        let write_id = stage.write_id.clone();
547        self.call_store(move |session| session.prepare_store_write_commit(stage))
548            .await?;
549        self.notify_write_status(write_id, WriteStatus::Publishing);
550        Ok(())
551    }
552
553    pub async fn prepare_merge_candidate_abandonment(
554        &self,
555        stage: MergeCandidateAbandonmentPreparation,
556    ) -> Result<(), DbError> {
557        let notified_write_id = stage.write_id.clone();
558        self.call_store(move |session| session.prepare_merge_candidate_abandonment(stage))
559            .await?;
560        self.notify_write_status(notified_write_id, WriteStatus::Publishing);
561        Ok(())
562    }
563
564    #[cfg(any(test, feature = "test-utils"))]
565    pub async fn enqueue_store_changeset_for_test(
566        &self,
567        changeset: Vec<u8>,
568    ) -> Result<(), DbError> {
569        let write_id = self.new_store_write_id();
570        self.call_store(move |session| {
571            session.enqueue_store_changeset_for_test(write_id, changeset)
572        })
573        .await
574    }
575}