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
31fn 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 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 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 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 ®istrations,
554 )?;
555 }
556 let retained = if materialize {
557 let materialization = VerifiedMergeMaterialization::verify(
558 &root,
559 &verified_commit,
560 ®istrations,
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 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 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 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 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}