Skip to main content

coven_database/store/store_session/
materialization.rs

1use std::collections::BTreeSet;
2
3use super::{
4    MergeMaterializationTransaction, StoreDatabase, StoreSession, StoreTransactionOutcome,
5    VerifiedStoreTransaction,
6};
7use crate::{
8    install_store_founder_state_on, AcceptedStorePublicationInterval, DbError,
9    VerifiedMergeMaterialization,
10};
11use coven_protocol::circle_activation::VerifiedCircleActivations;
12use coven_protocol::store_commit::{
13    ActivatedStoreDeviceRegistration, VerifiedStoreBatchCommit, VerifiedStoreDeviceOperations,
14};
15
16#[cfg(any(test, feature = "test-utils"))]
17fn reach_materialization_failure(
18    armed: &std::sync::Mutex<Option<crate::MergeMaterializationFailurePoint>>,
19    point: crate::MergeMaterializationFailurePoint,
20) -> Result<bool, DbError> {
21    let mut armed = armed
22        .lock()
23        .map_err(|_| DbError::Message("Merge materialization failure lock poisoned".to_string()))?;
24    if armed.as_ref() != Some(&point) {
25        return Ok(false);
26    }
27    armed.take();
28    Ok(true)
29}
30
31/// The bootstrap commits this database already materializes, which is to say
32/// the ones a previous run of this same installation landed before it stopped.
33///
34/// A plan carries only the history past the installed snapshot's coverage: the
35/// device that built it knew which snapshot this device installs and walked
36/// forward from that snapshot's tips. So a plan commit at or under a coverage
37/// tip is never the ordinary case — it is a plan built against a different
38/// history, a fork at that coordinate, or a snapshot that ran past the
39/// bootstrap cut. None can be installed over, so the join fails here instead of
40/// writing rows against an image that disagrees with them.
41fn device_join_bootstrap_represented_on(
42    tx: &rusqlite::Transaction<'_>,
43    commits: &[crate::DeviceJoinBootstrapCommit],
44) -> Result<BTreeSet<coven_protocol::store_commit::StoreBatchCommitRef>, DbError> {
45    let mut represented = BTreeSet::new();
46    let coverage = crate::store::materialized_commit_index::snapshot_coverage_on(tx)?;
47    for prepared in commits {
48        let stream_id = prepared.reference.coord.stream_id.to_string();
49        let sequence = prepared.reference.coord.sequence();
50        if let Some(existing) = crate::store::materialized_commit_index::materialized_commit_ref_on(
51            tx, &stream_id, sequence,
52        )? {
53            if existing != prepared.reference {
54                return Err(DbError::Message(format!(
55                    "device join bootstrap conflicts at {stream_id}/{sequence}"
56                )));
57            }
58            represented.insert(prepared.reference.clone());
59            continue;
60        }
61        if coverage
62            .get(&stream_id)
63            .is_some_and(|tip| sequence <= tip.coord.sequence())
64        {
65            return Err(DbError::Message(format!(
66                "device join bootstrap history at {stream_id}/{sequence} is not the history the \
67                 installed snapshot covers"
68            )));
69        }
70    }
71    Ok(represented)
72}
73
74impl VerifiedStoreTransaction<'_, '_, '_, '_> {
75    fn retain_received_merge_materialization(
76        &mut self,
77        materialization: &crate::PreparedMergeMaterialization,
78        receiver_wall_ms: u64,
79    ) -> Result<(), DbError> {
80        super::clock_floor::observe_circle_metadata(
81            &mut self.clock_floor,
82            materialization.circle_activations.circles(),
83            crate::IncomingTimestampPolicy::Received { receiver_wall_ms },
84        )?;
85        if !materialization.packages.is_empty()
86            && materialization.package_application
87                != Some(crate::RetainedPackageApplication::Received { receiver_wall_ms })
88        {
89            return Err(DbError::Message(
90                "received Merge packages carry another application timestamp".to_string(),
91            ));
92        }
93        let merge_transaction = MergeMaterializationTransaction::from_store(self.store);
94        merge_transaction
95            .record_prepared_materialization_authority(materialization)
96            .map_err(|error| DbError::context("received authority records", error))?;
97        let retained = merge_transaction
98            .retain_prepared_merge_materialization(self.authority, materialization)
99            .map_err(|error| {
100                DbError::context(
101                    format!(
102                        "received retained materialization {:?}",
103                        materialization.verified_commit.reference()
104                    ),
105                    error,
106                )
107            })?;
108        self.authority
109            .insert_verified(retained)
110            .map_err(|error| DbError::context("received authority cache", error))?;
111        #[cfg(any(test, feature = "test-utils"))]
112        if reach_materialization_failure(
113            self.merge_materialization_failure,
114            crate::MergeMaterializationFailurePoint::SummaryMaterialization,
115        )? {
116            return Err(DbError::Message(
117                "injected failure after Merge summary materialization".to_string(),
118            ));
119        }
120        for exclusion in materialization.circle_activations.local_exclusions() {
121            super::circle_operations::record_circle_close_exclusion_on(
122                self.store.transaction,
123                exclusion,
124            )?;
125        }
126        Ok(())
127    }
128
129    fn replay_received_merge_materializations(
130        &mut self,
131        candidate: &coven_protocol::store_commit::StoreBatchCommitRef,
132        local_store_membership: coven_protocol::membership::LocalStoreMembership,
133        routing_key: Option<&coven_protocol::circle::RowRoutingKey>,
134    ) -> Result<super::merge_materialization_transaction::AppliedMergeMaterialization, DbError>
135    {
136        let replayed = self.authority.replay_projection_watching_on(
137            self.store,
138            self.blob_decls,
139            self.gates,
140            self.synced_tables,
141            routing_key,
142            &BTreeSet::new(),
143            crate::ReplayJournal::Owed,
144            local_store_membership,
145            candidate,
146        )?;
147        let watched = replayed.watched_outcome().ok_or_else(|| {
148            DbError::Message("incoming Merge materialization was not replayed".to_string())
149        })?;
150        if let super::WatchedReplayOutcome::Held(reason) = watched {
151            return Ok(
152                super::merge_materialization_transaction::AppliedMergeMaterialization {
153                    outcome: crate::MaterializationOutcome::Held(reason),
154                    max_updated_at: None,
155                    write_status_notifications: Vec::new(),
156                },
157            );
158        }
159        let rows = replayed.install_on(self)?;
160        let max_updated_at = replayed.max_updated_at();
161        if max_updated_at > self.clock_floor {
162            self.clock_floor = max_updated_at.clone();
163        }
164        Ok(
165            super::merge_materialization_transaction::AppliedMergeMaterialization {
166                outcome: crate::MaterializationOutcome::Applied(rows),
167                max_updated_at,
168                write_status_notifications: Vec::new(),
169            },
170        )
171    }
172
173    pub(super) fn apply_received_store_publication_interval(
174        &mut self,
175        materializations: Vec<crate::PreparedMergeMaterialization>,
176        accepted: AcceptedStorePublicationInterval,
177        replay: coven_protocol::store_commit::VerifiedStorePublicationInterval,
178        snapshots: Vec<coven_protocol::store_commit::RetainedReplaySnapshotAuthority>,
179        local_store_membership: coven_protocol::membership::LocalStoreMembership,
180        schema_version: u32,
181        sync_routing_hash: coven_protocol::store_commit::ObjectHash,
182        routing_encryption: Option<&coven_keys::encryption::EncryptionService>,
183        routing_key: Option<coven_protocol::circle::RowRoutingKey>,
184        receiver_wall_ms: u64,
185        installed_checkpoint: Option<coven_protocol::store_commit::AcceptedStoreSnapshotRef>,
186    ) -> Result<
187        (
188            super::merge_materialization_transaction::AppliedMergeMaterialization,
189            Vec<coven_protocol::store_commit::StoreBatchCommitRef>,
190        ),
191        DbError,
192    > {
193        if replay.current() != accepted.interval().current() {
194            return Err(DbError::Message(
195                "Store replay and observed publication name different accepted tips".to_string(),
196            ));
197        }
198        // Acceptance and materialization become durable together, including the
199        // first observation in a newly opened founder database.
200        super::observed_store_publication::observe_store_publication_interval_on(
201            self.store, &accepted,
202        )?;
203        let baseline = super::retained_replay::load_replay_baseline_metadata_on(
204            super::StoreRecords::new(self.store.transaction, self.store.store_dir),
205        )?
206        .ok_or_else(|| DbError::Message("Store replay has no installed baseline".to_string()))?;
207        let anchor = match &baseline.authority {
208            crate::RetainedReplayAuthority::Genesis(authority) => {
209                coven_protocol::store_commit::StoreCurrentPublicationRecordBody::genesis(
210                    authority.store_root.store_root_hash,
211                )
212            }
213            crate::RetainedReplayAuthority::InstalledSnapshot(authority) => {
214                (*authority.metadata.publication_predecessor).clone()
215            }
216        };
217        if replay.previous() != &anchor {
218            return Err(DbError::Message(
219                "Store replay starts from another installed baseline".to_string(),
220            ));
221        }
222        let mut pending = std::collections::BTreeMap::new();
223        for prepared in materializations {
224            let reference = prepared.verified_commit.reference().clone();
225            if pending.insert(reference, prepared).is_some() {
226                return Err(DbError::Message(
227                    "duplicate Store publication materialization".to_string(),
228                ));
229            }
230        }
231        let mut snapshots = snapshots.into_iter();
232        let mut latest_snapshot = None;
233        let mut rebased_snapshot = installed_checkpoint;
234        let mut rows = Vec::new();
235        let mut max_updated_at = None;
236        let mut installed = Vec::new();
237        let mut complete = true;
238        // Snapshot boundaries close replay intervals. Within each interval,
239        // retain every accepted input before the canonical scheduler runs, so
240        // a concurrent dependency can be supplied by another accepted commit.
241        for entries in replay.entries().chunk_by(|left, right| {
242            matches!(
243                left.entry().payload,
244                coven_protocol::store_commit::StorePublicationPayload::Commit(_)
245            ) && matches!(
246                right.entry().payload,
247                coven_protocol::store_commit::StorePublicationPayload::Commit(_)
248            )
249        }) {
250            let entry = &entries[0];
251            match &entry.entry().payload {
252                coven_protocol::store_commit::StorePublicationPayload::Snapshot(reference) => {
253                    let authority = snapshots.next().ok_or_else(|| {
254                        DbError::Message(
255                            "Store snapshot publication has no verified baseline authority"
256                                .to_string(),
257                        )
258                    })?;
259                    if &authority.snapshot != reference
260                        || authority.store_root.store_root_hash != entry.reference().store_root_hash
261                        || authority.metadata.publication_predecessor.state_hash()
262                            != entry.entry().previous_state_hash
263                    {
264                        return Err(DbError::Message(
265                            "Store snapshot authority differs from its accepted publication"
266                                .to_string(),
267                        ));
268                    }
269                    if let Some((_, changes_publication_base)) = self
270                        .advance_snapshot_replay_baseline(
271                            &authority.store_root,
272                            &authority,
273                            schema_version,
274                            sync_routing_hash,
275                            routing_encryption,
276                        )
277                        .map_err(|error| {
278                            DbError::context("received snapshot baseline advance", error)
279                        })?
280                    {
281                        if changes_publication_base {
282                            rebased_snapshot =
283                                Some(coven_protocol::store_commit::AcceptedStoreSnapshotRef {
284                                    snapshot: authority.snapshot.clone(),
285                                    publication: entry.reference().clone(),
286                                });
287                        }
288                    }
289                    latest_snapshot = Some(entry.reference());
290                }
291                coven_protocol::store_commit::StorePublicationPayload::Commit(_) => {
292                    let references = entries
293                        .iter()
294                        .map(|entry| match &entry.entry().payload {
295                            coven_protocol::store_commit::StorePublicationPayload::Commit(
296                                reference,
297                            ) => reference,
298                            coven_protocol::store_commit::StorePublicationPayload::Snapshot(_) => {
299                                unreachable!("commit segments stop before every snapshot")
300                            }
301                        })
302                        .collect::<Vec<_>>();
303                    let mut replay = None;
304                    for reference in &references {
305                        if let Some(prepared) = pending.remove(*reference) {
306                            self.retain_received_merge_materialization(&prepared, receiver_wall_ms)
307                                .map_err(|error| {
308                                    DbError::context("received commit authority retention", error)
309                                })?;
310                            installed.push((*reference).clone());
311                            replay = Some(*reference);
312                        }
313                    }
314                    if let Some(reference) = replay {
315                        let applied = self
316                            .replay_received_merge_materializations(
317                                reference,
318                                local_store_membership,
319                                routing_key.as_ref(),
320                            )
321                            .map_err(|error| {
322                                DbError::context("received commit projection", error)
323                            })?;
324                        match applied.outcome {
325                            crate::MaterializationOutcome::Applied(changes) => rows.extend(changes),
326                            held @ crate::MaterializationOutcome::Held(_) => {
327                                return Ok((super::merge_materialization_transaction::AppliedMergeMaterialization {
328                                    outcome: held, max_updated_at: None, write_status_notifications: Vec::new(),
329                                }, Vec::new()));
330                            }
331                        }
332                        if applied.max_updated_at > max_updated_at {
333                            max_updated_at = applied.max_updated_at;
334                        }
335                    }
336                    for reference in references {
337                        let stream_id = reference.coord.stream_id.to_string();
338                        if crate::store::materialized_commit_index::materialized_commit_ref_on(
339                            self.store.transaction,
340                            &stream_id,
341                            reference.coord.sequence(),
342                        )? != Some(reference.clone())
343                        {
344                            complete = false;
345                        }
346                    }
347                    if !complete {
348                        // A snapshot closes the whole preceding interval. Keep
349                        // independent progress here, but do not fold past a held
350                        // commit or apply work on the far side of that boundary.
351                        break;
352                    }
353                }
354            }
355        }
356        if !pending.is_empty() || (complete && snapshots.next().is_some()) {
357            return Err(DbError::Message(
358                "prepared Store materialization is outside its accepted interval".to_string(),
359            ));
360        }
361        if let Some(snapshot) = rebased_snapshot {
362            rows.extend(
363                self.rebase_unpublished_store_writes(
364                    &snapshot,
365                    routing_key.as_ref(),
366                    local_store_membership,
367                )
368                .map_err(|error| DbError::context("received snapshot unpublished rebase", error))?,
369            );
370        }
371        if let Some(publication) = latest_snapshot {
372            super::observed_store_publication::retire_store_publication_prefix_before_snapshot_on(
373                self.store,
374                self.authority,
375                publication,
376            )
377            .map_err(|error| DbError::context("received snapshot publication retirement", error))?;
378        }
379        Ok((
380            super::merge_materialization_transaction::AppliedMergeMaterialization {
381                outcome: crate::MaterializationOutcome::Applied(rows),
382                max_updated_at,
383                write_status_notifications: Vec::new(),
384            },
385            installed,
386        ))
387    }
388
389    pub(super) fn install_replay_projection(
390        &self,
391        replay: &super::ReplayProjection,
392    ) -> Result<Vec<coven_foundation::changeset::RowChange>, DbError> {
393        let tx = self.store.transaction;
394        let mut host_changes = rusqlite::session::Session::new(tx).map_err(DbError::from)?;
395        for table in self.synced_tables {
396            host_changes
397                .attach(Some(table.name()))
398                .map_err(DbError::from)?;
399        }
400        let mut tables = crate::projection_table_names(self.gates.has_scoped_graph());
401        tables.extend(
402            self.synced_tables
403                .iter()
404                .map(|table| table.name().to_string()),
405        );
406        tables.sort();
407        tables.dedup();
408        let suspended_cleanup =
409            replay.suspend_blob_cleanup_for_restoration_on(tx, self.blob_decls)?;
410        let old_exact_bindings = super::local_blob_cleanup::exact_blob_bindings_on(tx)?;
411        tx.pragma_update(None, "defer_foreign_keys", "ON")
412            .map_err(DbError::from)?;
413        crate::store::store_session::StoreTransaction::new(tx, self.store.store_dir)
414            .replace_tables_from_projection(replay, &tables)?;
415        let violations: bool = tx
416            .query_row(
417                "SELECT EXISTS(SELECT 1 FROM pragma_foreign_key_check)",
418                [],
419                |row| row.get(0),
420            )
421            .map_err(DbError::from)?;
422        if violations {
423            let violation: (String, Option<i64>, String, i64) = tx
424                .query_row(
425                    "SELECT * FROM pragma_foreign_key_check LIMIT 1",
426                    [],
427                    |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
428                )
429                .map_err(DbError::from)?;
430            return Err(DbError::Message(format!(
431                "retained replay projection violates foreign keys: {violation:?}"
432            )));
433        }
434        let mut projection_changeset = Vec::new();
435        host_changes
436            .changeset_strm(&mut projection_changeset)
437            .map_err(DbError::from)?;
438        #[cfg(any(test, feature = "test-utils"))]
439        if reach_materialization_failure(
440            self.merge_materialization_failure,
441            crate::MergeMaterializationFailurePoint::ProjectionReplacement,
442        )? {
443            return Err(DbError::Message(
444                "injected failure after Merge projection replacement".to_string(),
445            ));
446        }
447        let old_projection =
448            crate::walk_old_changeset(&projection_changeset).map_err(DbError::Changeset)?;
449        let new_projection =
450            crate::walk_changeset(&projection_changeset).map_err(DbError::Changeset)?;
451        for intent in crate::local_blob_cleanup_intents::intents_from_changes(
452            self.blob_decls,
453            &old_projection,
454            &new_projection,
455        )
456        .map_err(DbError::from)?
457        {
458            super::local_blob_cleanup::record_obsolete_copy_intents_from_bindings_on(
459                tx,
460                self.blob_decls,
461                &intent,
462                &old_exact_bindings,
463            )?;
464        }
465        super::local_blob_cleanup::reevaluate_suspended_blob_cleanup_on(
466            tx,
467            self.blob_decls,
468            &suspended_cleanup,
469        )?;
470        crate::Database::cancel_transitions_for_deleted_roots_on(
471            tx,
472            &super::merge_materialization_transaction::deleted_rows(&new_projection),
473        )?;
474        Ok(new_projection)
475    }
476
477    fn complete_published_store_operation(
478        &self,
479        verified_commit: &VerifiedStoreBatchCommit,
480        acceptance: &crate::AcceptedStoreCommitEvidence,
481        history_evidence: &coven_protocol::store_commit::RetainedMergeCommitEvidence,
482        operation_object_ids: Option<Vec<coven_protocol::store_commit::ObjectHash>>,
483        membership_completion: Option<
484            coven_protocol::membership_mutation::StoreMembershipJournalCompletion,
485        >,
486    ) -> Result<(), DbError> {
487        let reference = verified_commit.reference();
488        history_evidence.validate_for(reference, verified_commit.value())?;
489        let store_transaction = MergeMaterializationTransaction::from_store(self.store);
490        if let Some(object_ids) = operation_object_ids {
491            store_transaction.activate_store_operation_remote_objects(reference, &object_ids)?;
492        }
493        if matches!(
494            super::active_store_publication::load_active_store_publication_on(
495                self.store.transaction
496            )?
497            .as_ref()
498            .map(|active| active.owner()),
499            Some(crate::ActiveStorePublicationOwner::DeviceJoin(_))
500        ) {
501            super::device_join_publication::complete_owner_device_join_publication_on(
502                self.store.transaction,
503                reference,
504                membership_completion.as_ref(),
505            )?;
506        }
507        if let Some(completion) = membership_completion {
508            store_transaction
509                .complete_membership_journal(
510                    completion,
511                    acceptance,
512                    verified_commit,
513                    history_evidence,
514                )
515                .map_err(|error| DbError::context("complete exact membership journal", error))?;
516        }
517        Ok(())
518    }
519
520    #[allow(clippy::too_many_arguments)]
521    fn materialize_published_store_operation(
522        &mut self,
523        root: coven_protocol::store_commit::StoreRootRef,
524        verified_commit: VerifiedStoreBatchCommit,
525        registrations: Vec<ActivatedStoreDeviceRegistration>,
526        device_operations: VerifiedStoreDeviceOperations,
527        circle_activations: VerifiedCircleActivations,
528        publication: crate::StoreCommitPublicationOutcome,
529        history_evidence: coven_protocol::store_commit::RetainedMergeCommitEvidence,
530        membership_objects: Option<crate::VerifiedMergeMembershipObjects>,
531        operation_object_ids: Option<Vec<coven_protocol::store_commit::ObjectHash>>,
532        membership_completion: Option<
533            coven_protocol::membership_mutation::StoreMembershipJournalCompletion,
534        >,
535    ) -> Result<Option<crate::OwnedVerifiedMergeMaterialization>, DbError> {
536        let publication = publication.resolve_installed_on(self.store, &verified_commit)?;
537        let materialize = publication.requires_materialization();
538        if materialize {
539            super::clock_floor::observe_circle_metadata(
540                &mut self.clock_floor,
541                circle_activations.circles(),
542                crate::IncomingTimestampPolicy::LocallyAuthored,
543            )?;
544        }
545        let tx = self.store.transaction;
546        let acceptance = publication.install_on(self.store, &verified_commit)?;
547        let authority = &mut *self.authority;
548        let store_transaction = MergeMaterializationTransaction::from_store(self.store);
549        if materialize && !registrations.is_empty() {
550            super::record_activated_store_device_registrations_on(
551                tx,
552                verified_commit.value(),
553                &registrations,
554            )?;
555        }
556        let retained = if materialize {
557            let materialization = VerifiedMergeMaterialization::verify(
558                &root,
559                &verified_commit,
560                &registrations,
561                &device_operations,
562                &circle_activations,
563                &acceptance,
564                &history_evidence,
565                membership_objects.as_ref(),
566                &[],
567                None,
568            )?;
569            let retained = store_transaction
570                .record_verified_merge_materialization(authority, materialization)
571                .map_err(|error| DbError::context("record exact Merge materialization", error))?;
572            authority.insert_verified(retained.clone())?;
573            Some(retained)
574        } else {
575            None
576        };
577        self.complete_published_store_operation(
578            &verified_commit,
579            &acceptance,
580            &history_evidence,
581            operation_object_ids,
582            membership_completion,
583        )?;
584        Ok(retained)
585    }
586
587    fn install_device_join_bootstrap(
588        &mut self,
589        root: coven_protocol::store_commit::StoreRootRef,
590        resolved: crate::ResolvedDeviceJoinBootstrap,
591    ) -> Result<(), DbError> {
592        let crate::ResolvedDeviceJoinBootstrap {
593            plan,
594            snapshot_circles,
595            mut row_data,
596            local_store_membership,
597            routing_key,
598            receiver_wall_ms,
599        } = resolved;
600        super::clock_floor::observe_circle_metadata(
601            &mut self.clock_floor,
602            row_data
603                .values()
604                .flat_map(|data| data.circle_activations.circles()),
605            crate::IncomingTimestampPolicy::Received { receiver_wall_ms },
606        )?;
607        let tx = self.store.transaction;
608        let blob_decls = self.blob_decls;
609        let gates = self.gates;
610        let synced_tables = self.synced_tables;
611        let authority = &mut *self.authority;
612        let installed_root = authority.root().clone();
613        if installed_root != root || plan.founder.store_root != root {
614            return Err(DbError::Message(
615                "device join bootstrap root differs from the installed exact root".to_string(),
616            ));
617        }
618        if !snapshot_circles.access.is_empty() || !snapshot_circles.bases.is_empty() {
619            let snapshot_floor = self.store.restore_device_join_snapshot_circles(
620                &root,
621                &snapshot_circles,
622                synced_tables,
623                blob_decls,
624                receiver_wall_ms,
625            )?;
626            if snapshot_floor > self.clock_floor {
627                self.clock_floor = snapshot_floor;
628            }
629            authority.forget_superseded_replay_baseline();
630        }
631        install_store_founder_state_on(
632            tx,
633            &root,
634            &plan.founder_reference,
635            &plan.founder,
636            &plan.founder_bytes,
637            &plan.genesis,
638        )?;
639        crate::set_protocol_state_on(
640            tx,
641            coven_protocol::membership::OWNER_PUBKEY_STATE_KEY,
642            &plan.founder.author_pubkey,
643        )?;
644        plan.membership.install_on(tx)?;
645        let publication_previous =
646            super::observed_store_publication::load_store_current_publication_on(tx)?;
647        if &**publication_previous.record() != plan.publication.interval().previous() {
648            return Err(DbError::Message(
649                "device join publication history starts from another snapshot boundary".to_string(),
650            ));
651        }
652        // Replay verifies every retained commit against its accepted
653        // publication. Install the verified interval first so those lookups
654        // see the same authority as the plan; the surrounding transaction
655        // still exposes the publication and materialized rows atomically.
656        super::observed_store_publication::install_store_publication_interval_on(
657            tx,
658            &publication_previous,
659            &plan.publication,
660        )?;
661        if let Some(snapshot) = plan.publication.interval().current().latest_snapshot() {
662            super::observed_store_publication::retire_store_publication_prefix_before_snapshot_on(
663                self.store,
664                authority,
665                &snapshot.publication,
666            )?;
667        }
668
669        let represented = device_join_bootstrap_represented_on(tx, &plan.commits)?;
670
671        // Row data has to be present before anything advances over it. A commit
672        // that names a Store package but resolved none would otherwise leave the
673        // joining device with an advanced position and no rows.
674        for prepared in &plan.commits {
675            if represented.contains(&prepared.reference) {
676                continue;
677            }
678            let commit = prepared.commit.value();
679            let resolved = row_data.get(&prepared.reference);
680            let carries_store_package = resolved.is_some_and(|data| {
681                data.packages.iter().any(|prepared| {
682                    matches!(
683                        prepared.package.audience(),
684                        coven_protocol::audience_package::PackageAudience::Store
685                    )
686                })
687            });
688            if resolved.is_none() || (commit.store_package().is_some() && !carries_store_package) {
689                return Err(DbError::Message(format!(
690                    "device join bootstrap cannot advance over unmaterialized row data at {}/{}",
691                    prepared.reference.coord.stream_id,
692                    prepared.reference.coord.sequence()
693                )));
694            }
695        }
696
697        let mut retained_any = false;
698        for prepared in plan.commits {
699            if represented.contains(&prepared.reference) {
700                continue;
701            }
702            let stream_id = prepared.reference.coord.stream_id.to_string();
703            if let Some(existing) =
704                crate::store::materialized_commit_index::materialized_commit_ref_on(
705                    tx,
706                    &stream_id,
707                    prepared.reference.coord.sequence(),
708                )?
709            {
710                if existing != prepared.reference {
711                    return Err(DbError::Message(format!(
712                        "device join bootstrap conflicts at {stream_id}/{}",
713                        prepared.reference.coord.sequence()
714                    )));
715                }
716                continue;
717            }
718            let data = row_data.remove(&prepared.reference).ok_or_else(|| {
719                DbError::Message(format!(
720                    "device join bootstrap has no resolved row data at {stream_id}/{}",
721                    prepared.reference.coord.sequence()
722                ))
723            })?;
724            let publication = plan
725                .publication
726                .accepted_commit(&prepared.commit)
727                .map_err(|error| DbError::context("device join accepted publication", error))?;
728            let materialization = crate::PreparedMergeMaterialization {
729                root: root.clone(),
730                verified_commit: prepared.commit,
731                acceptance: publication.into(),
732                history_evidence: prepared.history_evidence,
733                membership_objects: data.membership_objects,
734                membership_remote_objects: data.membership_remote_objects,
735                registrations: prepared.registrations,
736                package_application: (!data.packages.is_empty())
737                    .then_some(crate::RetainedPackageApplication::Received { receiver_wall_ms }),
738                packages: data.packages,
739                device_operations: prepared.device_operations,
740                circle_activations: data.circle_activations,
741            };
742            let merge_transaction = MergeMaterializationTransaction::from_store(self.store);
743            merge_transaction.record_prepared_materialization_authority(&materialization)?;
744            let retained = merge_transaction
745                .retain_prepared_merge_materialization(authority, &materialization)?;
746            authority.insert_verified(retained)?;
747            retained_any = true;
748        }
749        if !row_data.is_empty() {
750            return Err(DbError::Message(
751                "device join bootstrap resolved row data outside its exact history".to_string(),
752            ));
753        }
754        if !retained_any {
755            return Ok(());
756        }
757        let replayed = authority.replay_projection_result_on(
758            crate::store::store_session::StoreTransaction::new(tx, self.store.store_dir),
759            blob_decls,
760            gates,
761            synced_tables,
762            routing_key.as_ref(),
763            None,
764            crate::ReplayJournal::Owed,
765            local_store_membership,
766        )?;
767        replayed.install_on(self)?;
768        let max_updated_at = replayed.max_updated_at();
769        if max_updated_at > self.clock_floor {
770            self.clock_floor = max_updated_at.clone();
771        }
772        Ok(())
773    }
774
775    fn complete_owner_recovery(
776        &mut self,
777        verified_commit: VerifiedStoreBatchCommit,
778        publication: crate::StoreCommitPublicationOutcome,
779        history_evidence: coven_protocol::store_commit::RetainedMergeCommitEvidence,
780        registration: ActivatedStoreDeviceRegistration,
781        acceptance_result: coven_protocol::remote_object::RemoteObjectRecord,
782    ) -> Result<(), DbError> {
783        let root = self.authority.root().clone();
784        let publication = publication.resolve_installed_on(self.store, &verified_commit)?;
785        let proof = history_evidence
786            .membership_proof
787            .as_ref()
788            .ok_or_else(|| DbError::Message("Owner recovery has no membership proof".into()))?;
789        let membership_objects = crate::VerifiedMergeMembershipObjects::verify(
790            verified_commit.value(),
791            verified_commit.reference(),
792            &proof.entry_value,
793            &proof.head_value,
794            proof.head.clone(),
795        )?;
796        let coven_protocol::remote_object::RemoteObjectRecord::RetainedAuthority(result) =
797            &acceptance_result
798        else {
799            return Err(DbError::Message(
800                "Owner recovery result has another remote object domain".into(),
801            ));
802        };
803        if !matches!(&result.identity.domain, coven_protocol::remote_object::RetainedAuthorityObjectDomain::MembershipHeadAcceptance { head, .. } if head == &proof.head)
804        {
805            return Err(DbError::Message(
806                "Owner recovery result names another authority head".into(),
807            ));
808        }
809        let mut object_ids = membership_objects.object_ids().collect::<Vec<_>>();
810        object_ids.push(acceptance_result.object_id());
811        MergeMaterializationTransaction::from_store(self.store)
812            .activate_store_operation_remote_objects(verified_commit.reference(), &object_ids)?;
813        let accepted = match &publication {
814            crate::StoreCommitPublicationOutcome::Accepted { interval, .. } => {
815                interval.accepted_commit(&verified_commit)?
816            }
817            crate::StoreCommitPublicationOutcome::Installed(_) => {
818                let [activation] = verified_commit.device_registrations() else {
819                    return Err(DbError::Message(
820                        "installed Owner recovery must carry exactly one registration activation"
821                            .to_string(),
822                    ));
823                };
824                if !matches!(
825                    activation.authority,
826                    coven_protocol::store_commit::StoreDeviceRegistrationActivationRef::Recovery { .. }
827                ) || verified_commit.seq() != 1
828                    || verified_commit.author_registration != activation.registration
829                {
830                    return Err(DbError::Message(
831                        "installed Owner recovery commit differs from its recovery registration"
832                            .to_string(),
833                    ));
834                }
835                registration.verify_reference(activation)?;
836                let records =
837                    super::StoreRecords::new(self.store.transaction, self.store.store_dir);
838                let installed = records.activated_registration(&root, registration.reference())?;
839                let authority: coven_protocol::store_commit::StoreDeviceRegistrationActivation =
840                    serde_json::from_str(
841                        &records.activated_registration_authority(registration.reference())?,
842                    )
843                    .map_err(|error| {
844                        DbError::context("installed Owner recovery registration authority", error)
845                    })?;
846                if installed != *registration.value()
847                    || authority != *registration.activation()
848                    || records.local_activated_registration_ref()?.as_ref()
849                        != Some(registration.reference())
850                {
851                    return Err(DbError::Message(
852                        "installed Owner recovery differs from the local activated registration"
853                            .to_string(),
854                    ));
855                }
856                let evidence = publication.install_on(self.store, &verified_commit)?;
857                let completed = super::owner_recovery_publication::complete_matching_owner_recovery_publication_on(
858                    self.store,
859                    &verified_commit,
860                    &evidence,
861                )?;
862                if !completed && records.owner_recovery_publication_row()?.is_some() {
863                    return Err(DbError::Message(
864                        "installed Owner recovery differs from the pending publication journal"
865                            .to_string(),
866                    ));
867                }
868                return Ok(());
869            }
870        };
871        let reference = verified_commit.reference();
872        let device_operations =
873            VerifiedStoreDeviceOperations::without_exclusions(verified_commit.value())?;
874        let circle_activations =
875            VerifiedCircleActivations::membership_control(verified_commit.value(), reference)?;
876        let retained = self
877            .materialize_published_store_operation(
878                root,
879                verified_commit,
880                vec![registration],
881                device_operations,
882                circle_activations,
883                publication,
884                history_evidence,
885                Some(membership_objects),
886                None,
887                None,
888            )?
889            .ok_or_else(|| {
890                DbError::Message("new Owner recovery publication was already installed".to_string())
891            })?;
892        super::owner_recovery_publication::complete_owner_recovery_publication_on(
893            self.store,
894            retained.verified_commit(),
895            &accepted,
896        )?;
897        #[cfg(any(test, feature = "test-utils"))]
898        if reach_materialization_failure(
899            self.merge_materialization_failure,
900            crate::MergeMaterializationFailurePoint::SummaryMaterialization,
901        )? {
902            return Err(DbError::Message(
903                "injected failure after Merge summary materialization".to_string(),
904            ));
905        }
906        Ok(())
907    }
908}
909
910impl StoreSession<'_> {
911    fn apply_received_store_publication_interval(
912        &mut self,
913        materializations: Vec<crate::PreparedMergeMaterialization>,
914        accepted: AcceptedStorePublicationInterval,
915        replay: coven_protocol::store_commit::VerifiedStorePublicationInterval,
916        snapshots: Vec<coven_protocol::store_commit::RetainedReplaySnapshotAuthority>,
917        local_store_membership: coven_protocol::membership::LocalStoreMembership,
918        routing_encryption: Option<&coven_keys::encryption::EncryptionService>,
919        routing_key: Option<coven_protocol::circle::RowRoutingKey>,
920        receiver_wall_ms: u64,
921    ) -> Result<
922        (
923            super::merge_materialization_transaction::AppliedMergeMaterialization,
924            Vec<coven_protocol::store_commit::StoreBatchCommitRef>,
925        ),
926        DbError,
927    > {
928        let schema_version = self.schema_version;
929        let sync_routing_hash = self.sync_routing_hash;
930        let applied = self.verified_store_transaction(move |transaction| {
931            let applied = transaction.apply_received_store_publication_interval(
932                materializations,
933                accepted,
934                replay,
935                snapshots,
936                local_store_membership,
937                schema_version,
938                sync_routing_hash,
939                routing_encryption,
940                routing_key,
941                receiver_wall_ms,
942                None,
943            )?;
944            if matches!(applied.0.outcome, crate::MaterializationOutcome::Applied(_)) {
945                Ok(StoreTransactionOutcome::Commit(applied))
946            } else {
947                Ok(StoreTransactionOutcome::Rollback(applied))
948            }
949        })?;
950        Ok(applied)
951    }
952
953    #[allow(clippy::too_many_arguments)]
954    fn materialize_published_store_operation(
955        &mut self,
956        root: coven_protocol::store_commit::StoreRootRef,
957        verified_commit: VerifiedStoreBatchCommit,
958        registrations: Vec<ActivatedStoreDeviceRegistration>,
959        device_operations: VerifiedStoreDeviceOperations,
960        circle_activations: VerifiedCircleActivations,
961        publication: crate::StoreCommitPublicationOutcome,
962        history_evidence: coven_protocol::store_commit::RetainedMergeCommitEvidence,
963        membership_objects: Option<crate::VerifiedMergeMembershipObjects>,
964        operation_object_ids: Option<Vec<coven_protocol::store_commit::ObjectHash>>,
965        membership_completion: Option<
966            coven_protocol::membership_mutation::StoreMembershipJournalCompletion,
967        >,
968    ) -> Result<Option<crate::OwnedVerifiedMergeMaterialization>, DbError> {
969        self.verified_store_transaction(move |transaction| {
970            let retained = transaction.materialize_published_store_operation(
971                root,
972                verified_commit,
973                registrations,
974                device_operations,
975                circle_activations,
976                publication,
977                history_evidence,
978                membership_objects,
979                operation_object_ids,
980                membership_completion,
981            )?;
982            Ok(StoreTransactionOutcome::Commit(retained))
983        })
984    }
985
986    fn unrepresented_device_join_bootstrap_commits(
987        &mut self,
988        plan: crate::DeviceJoinBootstrapPlan,
989    ) -> Result<
990        (
991            crate::DeviceJoinBootstrapPlan,
992            Vec<coven_protocol::store_commit::StoreBatchCommitRef>,
993        ),
994        DbError,
995    > {
996        self.verified_store_transaction(move |transaction| {
997            let represented =
998                device_join_bootstrap_represented_on(transaction.store.transaction, &plan.commits)?;
999            let unrepresented = plan
1000                .commits
1001                .iter()
1002                .map(|prepared| prepared.reference.clone())
1003                .filter(|reference| !represented.contains(reference))
1004                .collect::<Vec<_>>();
1005            Ok(StoreTransactionOutcome::Rollback((plan, unrepresented)))
1006        })
1007    }
1008
1009    fn install_device_join_bootstrap(
1010        &mut self,
1011        root: coven_protocol::store_commit::StoreRootRef,
1012        resolved: crate::ResolvedDeviceJoinBootstrap,
1013    ) -> Result<(), DbError> {
1014        self.verified_store_transaction(move |transaction| {
1015            transaction.install_device_join_bootstrap(root, resolved)?;
1016            Ok(StoreTransactionOutcome::Commit(()))
1017        })
1018    }
1019
1020    fn complete_owner_recovery(
1021        &mut self,
1022        verified_commit: VerifiedStoreBatchCommit,
1023        publication: crate::StoreCommitPublicationOutcome,
1024        history_evidence: coven_protocol::store_commit::RetainedMergeCommitEvidence,
1025        registration: ActivatedStoreDeviceRegistration,
1026        acceptance_result: coven_protocol::remote_object::RemoteObjectRecord,
1027    ) -> Result<(), DbError> {
1028        self.verified_store_transaction(move |transaction| {
1029            transaction.complete_owner_recovery(
1030                verified_commit,
1031                publication,
1032                history_evidence,
1033                registration,
1034                acceptance_result,
1035            )?;
1036            Ok(StoreTransactionOutcome::Commit(()))
1037        })
1038    }
1039}
1040
1041impl StoreDatabase {
1042    pub async fn apply_received_store_publication_interval(
1043        &self,
1044        materializations: Vec<crate::PreparedMergeMaterialization>,
1045        accepted: AcceptedStorePublicationInterval,
1046        replay: coven_protocol::store_commit::VerifiedStorePublicationInterval,
1047        snapshots: Vec<crate::VerifiedStoreSnapshotAuthority>,
1048        local_store_membership: coven_protocol::membership::LocalStoreMembership,
1049        routing_encryption: Option<coven_keys::encryption::EncryptionService>,
1050        routing_key: Option<coven_protocol::circle::RowRoutingKey>,
1051        receiver_wall_ms: u64,
1052    ) -> Result<
1053        (
1054            crate::MaterializationOutcome,
1055            Vec<coven_protocol::store_commit::StoreBatchCommitRef>,
1056        ),
1057        DbError,
1058    > {
1059        let snapshots = snapshots
1060            .into_iter()
1061            .map(crate::VerifiedStoreSnapshotAuthority::into_authority)
1062            .collect();
1063        let (applied, installed) = self
1064            .call_store(move |session| {
1065                session.apply_received_store_publication_interval(
1066                    materializations,
1067                    accepted,
1068                    replay,
1069                    snapshots,
1070                    local_store_membership,
1071                    routing_encryption.as_ref(),
1072                    routing_key,
1073                    receiver_wall_ms,
1074                )
1075            })
1076            .await?;
1077        for (write_id, status) in applied.write_status_notifications {
1078            self.notify_write_status(write_id, status);
1079        }
1080        Ok((applied.outcome, installed))
1081    }
1082
1083    /// Complete an operation that an accepted pull already installed. Its
1084    /// exact candidate evidence still binds completion after its historical
1085    /// materialization inputs have been retired.
1086    pub async fn complete_installed_store_operation(
1087        &self,
1088        verified_commit: VerifiedStoreBatchCommit,
1089        acceptance: crate::AcceptedStoreCommitEvidence,
1090        history_evidence: coven_protocol::store_commit::RetainedMergeCommitEvidence,
1091        operation_object_ids: Option<Vec<coven_protocol::store_commit::ObjectHash>>,
1092        membership_completion: Option<
1093            coven_protocol::membership_mutation::StoreMembershipJournalCompletion,
1094        >,
1095    ) -> Result<crate::AcceptedStoreCommitEvidence, DbError> {
1096        self.call_store(move |session| {
1097            session.verified_store_transaction(move |transaction| {
1098                let publication = crate::StoreCommitPublicationOutcome::Installed(acceptance)
1099                    .resolve_installed_on(transaction.store, &verified_commit)?;
1100                let acceptance = publication.install_on(transaction.store, &verified_commit)?;
1101                transaction.complete_published_store_operation(
1102                    &verified_commit,
1103                    &acceptance,
1104                    &history_evidence,
1105                    operation_object_ids,
1106                    membership_completion,
1107                )?;
1108                Ok(StoreTransactionOutcome::Commit(acceptance))
1109            })
1110        })
1111        .await
1112    }
1113
1114    #[allow(clippy::too_many_arguments)]
1115    pub async fn materialize_published_store_operation(
1116        &self,
1117        root: coven_protocol::store_commit::StoreRootRef,
1118        verified_commit: VerifiedStoreBatchCommit,
1119        registrations: Vec<ActivatedStoreDeviceRegistration>,
1120        device_operations: VerifiedStoreDeviceOperations,
1121        circle_activations: VerifiedCircleActivations,
1122        publication: crate::StoreCommitPublicationOutcome,
1123        history_evidence: coven_protocol::store_commit::RetainedMergeCommitEvidence,
1124        membership_objects: Option<crate::VerifiedMergeMembershipObjects>,
1125        operation_object_ids: Option<Vec<coven_protocol::store_commit::ObjectHash>>,
1126        membership_completion: Option<
1127            coven_protocol::membership_mutation::StoreMembershipJournalCompletion,
1128        >,
1129    ) -> Result<Option<crate::OwnedVerifiedMergeMaterialization>, DbError> {
1130        self.call_store(move |session| {
1131            session.materialize_published_store_operation(
1132                root,
1133                verified_commit,
1134                registrations,
1135                device_operations,
1136                circle_activations,
1137                publication,
1138                history_evidence,
1139                membership_objects,
1140                operation_object_ids,
1141                membership_completion,
1142            )
1143        })
1144        .await
1145    }
1146
1147    /// The plan commits whose rows this database does not already materialize.
1148    /// The joining device resolves row data for exactly these before installing.
1149    pub async fn unrepresented_device_join_bootstrap_commits(
1150        &self,
1151        plan: crate::DeviceJoinBootstrapPlan,
1152    ) -> Result<
1153        (
1154            crate::DeviceJoinBootstrapPlan,
1155            Vec<coven_protocol::store_commit::StoreBatchCommitRef>,
1156        ),
1157        DbError,
1158    > {
1159        self.call_store(move |session| session.unrepresented_device_join_bootstrap_commits(plan))
1160            .await
1161    }
1162
1163    pub async fn install_device_join_bootstrap(
1164        &self,
1165        root: coven_protocol::store_commit::StoreRootRef,
1166        resolved: crate::ResolvedDeviceJoinBootstrap,
1167    ) -> Result<(), DbError> {
1168        self.call_store(move |session| session.install_device_join_bootstrap(root, resolved))
1169            .await
1170    }
1171
1172    pub async fn complete_owner_recovery(
1173        &self,
1174        verified_commit: VerifiedStoreBatchCommit,
1175        publication: crate::StoreCommitPublicationOutcome,
1176        history_evidence: coven_protocol::store_commit::RetainedMergeCommitEvidence,
1177        registration: ActivatedStoreDeviceRegistration,
1178        acceptance_result: coven_protocol::remote_object::RemoteObjectRecord,
1179    ) -> Result<(), DbError> {
1180        self.call_store(move |session| {
1181            session.complete_owner_recovery(
1182                verified_commit,
1183                publication,
1184                history_evidence,
1185                registration,
1186                acceptance_result,
1187            )
1188        })
1189        .await
1190    }
1191}