Skip to main content

coven_replication/sync/store/commit_publication/operation/
facades.rs

1use super::*;
2
3impl<'storage> AuthorizedWriterOperation<'storage> {
4    pub(crate) async fn history_has_only_acknowledgements(
5        &mut self,
6        previous: &coven_protocol::store_commit::StoreHistoryCut,
7        current: &coven_protocol::store_commit::StoreHistoryCut,
8    ) -> Result<bool, crate::sync::store::pull::StorePullError> {
9        self.history
10            .history_has_only_acknowledgements(previous, current)
11            .await
12    }
13
14    pub(super) fn membership_objects(&self) -> StoreMembershipObjectVerifier<'_, 'storage> {
15        self.history.membership_objects()
16    }
17
18    pub(crate) fn store_root(&self) -> &coven_protocol::store_commit::StoreRootRef {
19        self.history.root()
20    }
21
22    pub(crate) async fn snapshot_publication(
23        &self,
24    ) -> crate::sync::store::snapshots::AuthorizedSnapshotPublication<'_> {
25        crate::sync::store::snapshots::AuthorizedSnapshotPublication::begin(
26            &self.database,
27            self.storage.as_ref(),
28        )
29        .await
30    }
31
32    pub(crate) async fn resume_snapshot_publication(
33        &mut self,
34    ) -> Result<
35        Option<coven_protocol::store_commit::SnapshotMeta>,
36        crate::sync::store::snapshots::SnapshotError,
37    > {
38        self.snapshots().resume_pending_publication().await
39    }
40
41    pub(crate) async fn publish_store_snapshot(
42        &mut self,
43        pending: &coven_database::DurableSnapshotPublication,
44        objects: &crate::sync::store::snapshots::AuthorizedSnapshotPublication<'_>,
45    ) -> Result<
46        crate::sync::store::authorization::history::publication::StoreSnapshotPublicationAttemptOutcome,
47        crate::sync::store::snapshots::SnapshotError,
48    >{
49        self.writer
50            .publish_store_snapshot(&mut self.history, &mut self.membership, pending, objects)
51            .await
52    }
53
54    pub(crate) async fn capture_current_store_snapshot_cut(
55        &self,
56    ) -> Result<
57        (
58            coven_database::CreatedSnapshot,
59            coven_protocol::store_commit::CommitFrontier,
60        ),
61        coven_database::DbError,
62    > {
63        self.history.capture_current_store_snapshot_cut().await
64    }
65
66    pub(crate) fn protocol_root(&self) -> &coven_protocol::store_commit::StoreProtocolRoot {
67        &self.history.verified_root_object().value
68    }
69
70    pub(crate) async fn prepare_wrapped_key(
71        &self,
72        recipient: &str,
73        value: coven_protocol::wrapped_store_key::WrappedStoreKey,
74    ) -> Result<
75        coven_protocol::wrapped_store_key::PreparedWrappedStoreKey,
76        coven_protocol::objects::StorageError,
77    > {
78        self.keyrings.prepare(recipient, value).await
79    }
80
81    /// Select the exact author stream without overwriting its committed prefix.
82    /// Streams are persisted per database, so independently restored devices use
83    /// different streams; copied state that reuses one exposes an immutable fork.
84    pub(super) async fn select_membership_author_stream(
85        &self,
86        chain: &coven_protocol::membership::MembershipChain,
87    ) -> Result<
88        coven_protocol::membership::AuthorStreamId,
89        crate::sync::store::commit_publication::membership::MembershipMutationError,
90    > {
91        self.history
92            .select_membership_author_stream(chain, &self.writer.author_pubkey())
93            .await
94    }
95
96    pub(crate) async fn resolve_accepted_snapshot(
97        &mut self,
98    ) -> Result<
99        Result<
100            crate::sync::store::commit_verification::merge_history::SelectedStoreSnapshot,
101            crate::sync::store::ReplayBaselineDecline,
102        >,
103        crate::sync::store::acknowledgements::StoreAckError,
104    > {
105        self.history.resolve_accepted_snapshot().await
106    }
107
108    pub(super) async fn stage_verified_blob_plaintext(
109        &self,
110        authority: &coven_protocol::blob::RowBlobAuthority,
111        stored: &coven_protocol::blob::locator::StoredBlobRef,
112        destination: &std::path::Path,
113    ) -> Result<coven_foundation::local_file::AtomicStagedFile, crate::sync::BlobCacheError> {
114        let stage = self
115            .store_dir
116            .stage_atomic_file(destination)
117            .await
118            .map_err(crate::sync::BlobCacheError::File)?;
119        self.history
120            .stage_verified_blob_plaintext(
121                authority,
122                stored,
123                stage,
124                coven_storage::cloud::no_download_progress(),
125            )
126            .await
127    }
128
129    pub(super) async fn authorize_retained_preparation(
130        &self,
131        order: &coven_protocol::store_commit::StoreCommitOrder,
132        membership_heads: &[coven_protocol::membership::MembershipHeadRef],
133    ) -> Result<
134        crate::sync::store::commit_verification::merge_history::MergeOutboundAuthorization,
135        crate::sync::store::pull::StorePullError,
136    > {
137        self.writer
138            .authorize_retained_preparation(&self.history, order, membership_heads)
139            .await
140    }
141
142    /// Seed the verifier from retained history before a walk over it, so the
143    /// walk reads nothing from the provider.
144    /// Retire the owner's journal for every join whose device has arrived.
145    ///
146    /// The owner's half of a join ends at a published activation commit, and
147    /// until now it ended there permanently: the row sat at
148    /// `ActivationPrepared` for the life of the store, still offering to hand
149    /// the activation over, because the owner has no artifact by which it could
150    /// learn the joining device took it — the same asymmetry that makes the
151    /// joiner, not the owner, delete the attempt's transport slots.
152    ///
153    /// The arrival it can see is the joined device's own first commit. Every
154    /// other trace of the join is something the owner wrote: the registration
155    /// goes Active from the owner's own activation commit, so it says nothing
156    /// about whether the device ever ran. A stream in the materialized frontier
157    /// under that device's announcement stream id is a commit the device signed
158    /// and this device verified, which it can only have published after
159    /// installing the Store.
160    ///
161    /// Reads nothing from the provider: the stream id is derived from the
162    /// registration the journal already holds, and the frontier is the row the
163    /// cycle reads anyway. Each retirement is one row delete, so a cycle that
164    /// fails partway leaves the rest for the next one to find.
165    pub(crate) async fn retire_arrived_device_joins(
166        &self,
167    ) -> Result<usize, crate::sync::store::DeviceJoinError> {
168        let awaiting = self.database.owner_device_joins_awaiting_arrival().await?;
169        if awaiting.is_empty() {
170            return Ok(0);
171        }
172        let frontier = self.database.materialized_frontier().await?;
173        let store_root_hash = self.store_root().store_root_hash;
174        let mut retired = 0;
175        for (attempt_id, registration) in awaiting {
176            let stream =
177                coven_protocol::store_commit::StreamActivation::device_authorized_stream_id(
178                    store_root_hash,
179                    &registration,
180                    coven_protocol::store_commit::StreamAnchorDomain::StoreAnnouncements,
181                );
182            if !frontier.contains_key(&stream.to_string()) {
183                continue;
184            }
185            self.database
186                .retire_device_join(attempt_id, crate::sync::store::DeviceJoinRole::Owner)
187                .await?;
188            retired += 1;
189        }
190        Ok(retired)
191    }
192
193    pub(crate) async fn seed_retained_history(
194        &mut self,
195    ) -> Result<(), crate::sync::store::pull::StorePullError> {
196        self.history.seed_retained_history().await
197    }
198
199    pub(super) async fn prepare_merge_history_successor(
200        &self,
201        commit: &coven_protocol::store_commit::VerifiedStoreBatchCommit,
202        membership: &coven_protocol::membership::MembershipChain,
203        recovery_author: Option<&coven_protocol::store_commit::StoreDeviceRegistrationRef>,
204        predecessor_state: &coven_protocol::store_commit::ResolvedStoreDeviceState,
205        state_after: &coven_protocol::store_commit::ResolvedStoreDeviceState,
206        evidence: crate::sync::store::commit_verification::merge_history::MergeHistorySuccessorEvidence,
207    ) -> Result<
208        crate::sync::store::commit_verification::merge_history::PreparedMergeHistorySuccessor,
209        crate::sync::store::pull::StorePullError,
210    > {
211        self.history
212            .prepare_merge_history_successor(
213                commit,
214                membership,
215                recovery_author,
216                predecessor_state,
217                state_after,
218                evidence,
219            )
220            .await
221    }
222
223    pub(super) async fn upload_commit(
224        &self,
225        candidate: &commit_plan::PreparedStoreOperationCommit,
226    ) -> Result<(), StoreError> {
227        let stream_id = candidate.reference.coord.stream_id;
228        let context = coven_protocol::objects::ProtocolObjectContext::signed_plaintext(
229            candidate.commit.store_root_hash,
230            coven_protocol::objects::ProtocolObjectDomain::StoreCommit,
231        );
232        let prefix = coven_protocol::store_commit::commit_semantic_prefix(
233            candidate.commit.candidate_family(),
234            &stream_id.to_string(),
235            candidate.commit.seq(),
236            candidate.commit.commit_hash(),
237        );
238        self.storage
239            .as_ref()
240            .create_verified_protocol_object(
241                &context,
242                &candidate.prepared_commit()?,
243                &prefix,
244                &candidate.commit.to_bytes(),
245            )
246            .await
247            .map_err(StoreError::prepared_object)
248    }
249
250    pub async fn pull(
251        &mut self,
252        routing_encryption: Option<&coven_keys::encryption::EncryptionService>,
253    ) -> Result<crate::sync::store::StorePullResult, SyncCycleFailure> {
254        let membership = self.membership.clone();
255        let execution = self
256            .writer
257            .pull(&mut self.history, &membership, routing_encryption)
258            .await
259            .map_err(|error| SyncCycleFailure::operation("pull Store commits", error))?;
260        self.membership = execution.membership;
261        Ok(execution.result)
262    }
263
264    pub(crate) fn require_current_owner(
265        &self,
266        author_pubkey: &str,
267    ) -> Result<(), coven_protocol::membership::MembershipError> {
268        if self.membership.is_owner_now(author_pubkey) {
269            Ok(())
270        } else {
271            Err(
272                coven_protocol::membership::MembershipError::SignerIsNotOwner(
273                    author_pubkey.to_string(),
274                ),
275            )
276        }
277    }
278
279    pub(crate) async fn prepare_merge_snapshot_history_summary(
280        &self,
281        coverage: &coven_protocol::store_commit::CommitFrontier,
282        membership: &coven_protocol::membership::MembershipChain,
283        state: &coven_protocol::store_commit::ResolvedStoreDeviceState,
284        publication: &coven_protocol::store_commit::StoreCurrentPublicationRecord,
285    ) -> Result<
286        coven_protocol::store_commit::RetainedVerifiedMergeHistorySummary,
287        crate::sync::store::pull::StorePullError,
288    > {
289        self.writer
290            .prepare_merge_snapshot_history_summary(
291                &self.history,
292                coverage,
293                membership,
294                state,
295                publication,
296            )
297            .await
298    }
299
300    /// The membership objects a reader of this Store's current frontier would
301    /// otherwise fetch one at a time — what a snapshot publishes as its
302    /// membership rollup.
303    ///
304    /// This runs the anchored walk again rather than keeping what an earlier
305    /// one read, because the rollup has to describe the whole chain and not
306    /// whatever part of it this operation happened to touch. The walk is
307    /// served from this verifier's own slot and object memos, so on a device
308    /// that has already resolved its membership this costs the terminating
309    /// probe per stream and nothing else.
310    pub(crate) async fn membership_rollup_parts(
311        &mut self,
312        membership: &coven_protocol::membership::MembershipChain,
313    ) -> Result<
314        Vec<coven_protocol::store_commit::MembershipRollupStream>,
315        crate::sync::store::membership::AnchoredChainError,
316    > {
317        self.history.membership_rollup_parts(membership).await
318    }
319
320    pub(crate) fn snapshots(
321        &mut self,
322    ) -> crate::sync::store::snapshots::AuthorizedSnapshots<'_, 'storage> {
323        let database = self.database.clone();
324        let storage = Arc::clone(self.storage);
325        let store_dir = self.store_dir;
326        let local_writer = Arc::clone(&self.writer);
327        crate::sync::store::snapshots::AuthorizedSnapshots::new(
328            self,
329            database,
330            storage,
331            store_dir,
332            local_writer,
333        )
334    }
335
336    pub(crate) fn acknowledgements(
337        &mut self,
338    ) -> crate::sync::store::acknowledgements::AuthorizedAcknowledgements<'_, 'storage> {
339        let database = self.database.clone();
340        let storage = Arc::clone(self.storage);
341        let local_writer = Arc::clone(&self.writer);
342        crate::sync::store::acknowledgements::AuthorizedAcknowledgements::new(
343            self,
344            database,
345            storage,
346            local_writer,
347        )
348    }
349
350    pub(crate) fn reclaim_history(
351        &mut self,
352    ) -> crate::sync::store::reclaim::ReclaimHistory<'_, 'storage> {
353        self.history.reclaim()
354    }
355
356    pub(crate) fn owner_promotion(
357        &mut self,
358    ) -> crate::sync::store::owner_role_promotion::AuthorizedOwnerPromotion<'_, 'storage> {
359        let database = self.database.clone();
360        let storage = self.storage.clone();
361        let root = self.store_root().clone();
362        crate::sync::store::owner_role_promotion::AuthorizedOwnerPromotion::new(
363            self, database, storage, root,
364        )
365    }
366
367    pub(crate) fn owner_promotion_history(
368        &mut self,
369    ) -> crate::sync::store::owner_role_promotion::OwnerPromotionHistory<'_, 'storage> {
370        self.history.owner_promotion()
371    }
372
373    pub(crate) async fn refresh_authorization_state(
374        &self,
375        cipher: &dyn coven_storage::CloudSyncCipherStateAccess,
376        pending_rotation: &dyn coven_storage::CloudSyncRotationStateAccess,
377        master_keys: Option<&dyn coven_keys::keys::MasterKeyCustody>,
378    ) -> Result<(), SyncCycleFailure> {
379        let result = async {
380            if cipher.is_plaintext() {
381                tracing::debug!("refresh: plaintext home, nothing to refresh");
382                return Ok(());
383            }
384
385            let recipient = self.writer.author_pubkey();
386            let wrapped_keys = self
387                .membership
388                .wrapped_key_authority_for(&recipient)
389                .map_err(AuthorizationRefreshError::Membership)?;
390            if wrapped_keys.is_empty() {
391                tracing::debug!(
392                    "refresh: no activated wrapped key for this device; keeping the live key"
393                );
394                return Ok(());
395            }
396
397            match self.keyrings.open(&self.membership).await {
398                Ok(new_encryption) => {
399                    let merged = cipher
400                        .merged_keyring(&new_encryption)
401                        .map_err(AuthorizationRefreshError::InvalidKeyring)?;
402                    if merged.merged_key_count() == merged.live_key_count() {
403                        if pending_rotation.gate().is_some() {
404                            let gate = self
405                                .database
406                                .complete_peer_rotation_adoption(merged.merged_generation())
407                                .await
408                                .map_err(AuthorizationRefreshError::Database)?;
409                            pending_rotation.install_durable_gate(gate);
410                        }
411                        tracing::debug!(
412                            "refresh: wrapped store key is already held by the live keyring"
413                        );
414                    } else {
415                        let gate = self
416                            .database
417                            .record_peer_rotation(merged.merged_generation())
418                            .await
419                            .map_err(AuthorizationRefreshError::Database)?;
420                        pending_rotation.install_durable_gate(Some(gate));
421                        match master_keys {
422                            None => {
423                                tracing::info!(
424                                    committed_generation = merged.merged_generation(),
425                                    "refresh: found a rotated store key but this cycle has no \
426                                     master-key custody to adopt it; sealing is paused until a \
427                                     cycle with custody adopts it"
428                                );
429                            }
430                            Some(master_keys) => {
431                                let adopted = cipher
432                                    .adopt_key_rotation(&new_encryption, master_keys)
433                                    .map_err(AuthorizationRefreshError::KeyAdoption)?;
434                                let gate = self
435                                    .database
436                                    .complete_peer_rotation_adoption(adopted.generation())
437                                    .await
438                                    .map_err(AuthorizationRefreshError::Database)?;
439                                pending_rotation.install_durable_gate(gate);
440                                tracing::info!(
441                                    fingerprint = adopted.fingerprint(),
442                                    "Adopted rotated store key"
443                                );
444                            }
445                        }
446                    }
447                }
448                Err(error) => return Err(AuthorizationRefreshError::WrappedKey(error)),
449            }
450
451            Ok(())
452        }
453        .await;
454
455        result.map_err(|error| SyncCycleFailure::operation("refresh authorization state", error))
456    }
457
458    pub(crate) fn circles(
459        &mut self,
460    ) -> crate::sync::store::circles::AuthorizedCircleWriter<'_, 'storage> {
461        let database = self.database.clone();
462        let storage = Arc::clone(self.storage);
463        let store_dir = self.store_dir;
464        let root = self.store_root().clone();
465        let local_writer = Arc::clone(&self.writer);
466        crate::sync::store::circles::AuthorizedCircleWriter::from_parts(
467            self,
468            database,
469            storage,
470            store_dir,
471            root,
472            local_writer,
473        )
474    }
475
476    pub(crate) fn circle_history(
477        &mut self,
478    ) -> crate::sync::store::commit_publication::circles::VerifiedCircleHistory<'_, 'storage> {
479        self.history.circles()
480    }
481
482    pub(crate) fn join_history(
483        &mut self,
484    ) -> crate::sync::store::device_join::history::DeviceJoinHistory<'_, 'storage> {
485        self.history.device_join()
486    }
487
488    pub(crate) fn device_exclusion_history(
489        &mut self,
490    ) -> crate::sync::store::device_exclusion::DeviceExclusionHistory<'_, 'storage> {
491        self.history.device_exclusion()
492    }
493
494    pub(crate) fn device_exclusion(
495        &mut self,
496    ) -> crate::sync::store::device_exclusion::AuthorizedDeviceExclusion<'_, 'storage> {
497        let database = self.database.clone();
498        let storage = Arc::clone(self.storage);
499        crate::sync::store::device_exclusion::AuthorizedDeviceExclusion::new(
500            self, database, storage,
501        )
502    }
503
504    pub(crate) fn join_operation(
505        &mut self,
506    ) -> crate::sync::store::commit_publication::device_join::AuthorizedJoin<'_, 'storage> {
507        let database = self.database.clone();
508        let storage = Arc::clone(self.storage);
509        let root = self.store_root().clone();
510        let verified_root = self.history.verified_root_object().clone();
511        let membership = self.membership.clone();
512        let local_writer = Arc::clone(&self.writer);
513        crate::sync::store::commit_publication::device_join::AuthorizedJoin::from_parts(
514            self,
515            database,
516            storage,
517            root,
518            verified_root,
519            membership,
520            local_writer,
521        )
522    }
523
524    pub(super) async fn membership_mutation_permit(
525        &self,
526    ) -> coven_database::store::MembershipMutationPermit {
527        self.database.membership_mutation_permit().await
528    }
529
530    pub(super) fn writer_pubkey(&self) -> String {
531        self.writer.author_pubkey()
532    }
533
534    pub(crate) fn local_author_pubkey(&self) -> String {
535        self.writer.author_pubkey()
536    }
537
538    pub(crate) fn is_local_registration(
539        &self,
540        registration: &coven_protocol::store_commit::StoreDeviceRegistrationRef,
541    ) -> bool {
542        self.writer.is_authored_by_registration(registration)
543    }
544
545    pub(crate) fn is_current_owner(
546        &self,
547        membership: &coven_protocol::membership::MembershipChain,
548    ) -> bool {
549        self.writer.is_current_owner(membership)
550    }
551
552    pub(crate) fn matches_local_author(
553        &self,
554        registration: &coven_protocol::store_commit::StoreDeviceRegistrationRef,
555        author_pubkey: &str,
556    ) -> bool {
557        self.writer.matches_author(registration, author_pubkey)
558    }
559
560    pub(crate) fn grant_authorized_stream_id(
561        &self,
562        grant: &coven_protocol::membership::MembershipGrantId,
563        domain: coven_protocol::store_commit::StreamAnchorDomain,
564    ) -> coven_protocol::membership::AuthorStreamId {
565        self.writer
566            .grant_authorized_stream_id(self.store_root().store_root_hash, grant, domain)
567    }
568}