Skip to main content

coven_replication/sync/store/commit_verification/
merge_history.rs

1use super::commit::{
2    StoreCommitVerifier, StoreMembershipObjectVerifier, VerifiedMergeMembershipClosure,
3};
4use crate::sync::store::pull;
5use crate::sync::store::pull::*;
6use crate::sync::store::StoreError;
7use coven_database::{
8    DeviceJoinBootstrapActivation, DeviceJoinBootstrapCommit, DeviceJoinBootstrapPlan,
9};
10use coven_database::{VerifiedAcknowledgedStoreSnapshot, VerifiedStoreSnapshotAuthority};
11use coven_protocol::circle_activation::VerifiedCircleActivations;
12use coven_protocol::circle_control::StoreMembershipStateRef;
13use coven_protocol::membership::{MembershipChain, MembershipStatus};
14use coven_protocol::objects::{
15    ExactObjectRef, ProtocolObjectContext, ProtocolObjectDomain, StorageError,
16};
17use coven_protocol::objects::{StoreObjectError, VerifiedObject};
18use coven_protocol::store_commit::{
19    ActivatedStoreDeviceRegistration, ActivatedStoreDeviceRegistrationRef, CommitFrontier,
20    DeviceJoinAttemptDecisionRef, DeviceStreamAnchor, ObjectHash,
21    OpenedRetainedMergeHistorySummary, OwnerRecoveryNode, OwnerRecoveryNodeRef,
22    ReferencedStoreDeviceRegistration, ResolvedStoreDeviceState,
23    RetainedVerifiedMergeHistorySummary, StoreBatchCommit, StoreBatchCommitRef, StoreCommitCoord,
24    StoreDeviceHead, StoreDeviceId, StoreDeviceProposalState, StoreDeviceRegistration,
25    StoreDeviceRegistrationActivation, StoreDeviceRegistrationActivationRef,
26    StoreDeviceRegistrationOrigin, StoreDeviceRegistrationRef, StoreDeviceStateRef,
27    StoreDeviceStatus, StoreHistoryCut, StoreProtocolError, StoreRootRef, VerifiedStoreBatchCommit,
28    VerifiedStoreDeviceOperations,
29};
30use coven_protocol::store_commit::{
31    SnapshotMeta, StoreAck, StoreAckRef, StoreDeviceExclusionOutcomeRef,
32    StoreDeviceExclusionProposalRef, StoreDeviceHeadRef, StoreSnapshotRef,
33    VerifiedDeviceExclusionOutcome, VerifiedDeviceExclusionProposal,
34};
35use coven_protocol::{
36    causal_grants, membership as protocol_membership, provider, remote_object, store_commit,
37};
38use std::collections::{BTreeMap, BTreeSet};
39
40use super::commit::DeviceStateResolver;
41use crate::sync::store::device_join;
42
43mod device_join_verification;
44mod loaders;
45mod membership_control;
46mod predecessor;
47use predecessor::{
48    predecessor_verifies_provider_administrator, predecessor_verifies_provider_administrator_grant,
49};
50mod promotion;
51mod rollup;
52mod snapshots;
53mod stream;
54mod successor;
55pub use membership_control::VerifiedMergeMembershipPrefix;
56pub(crate) use membership_control::{
57    merge_membership_state_ref, verified_merge_membership_prefix,
58    verify_merge_membership_state_ref, VerifiedMergeMembershipControl,
59    VerifiedMergeMembershipHeadActivation, VerifiedMergePrefixHeadStatus,
60};
61pub(crate) use predecessor::{
62    predecessor_verifies_owner, PredecessorSearch, VerifiedMergePredecessorHistory,
63};
64pub(crate) use promotion::{
65    VerifiedMergeConflictResolutionActivation, VerifiedOwnerPromotionRequestActivation,
66};
67pub(crate) use snapshots::{
68    weigh_every_snapshot, SelectedAcknowledgedStoreSnapshot, SelectedInstallableStoreSnapshot,
69    SelectedReplayBaselineRetirement, SelectedStoreSnapshot, StoreSnapshotDescentStep,
70};
71pub use successor::MergeHistorySuccessorEvidence;
72pub use successor::PreparedMergeHistorySuccessor;
73pub(crate) use successor::{
74    compose_merge_snapshot_history_summary, compose_verified_merge_snapshot_history_summary,
75    validate_composed_snapshot_history_summary,
76};
77#[cfg(test)]
78pub(crate) use successor::{insert_latest_acknowledgement, merge_retained_merge_history};
79pub(super) mod join_validation;
80mod membership;
81use membership::VerifiedPrefixMembershipActivation;
82pub(crate) mod registration;
83use join_validation::*;
84pub(crate) use registration::RegistrationLoadError;
85use registration::*;
86
87pub(crate) struct VerifiedMergeHistoryCommit {
88    pub(crate) verified: VerifiedStoreBatchCommit,
89    pub(crate) predecessor_membership: MembershipChain,
90    pub(crate) predecessor_state: ResolvedStoreDeviceState,
91    pub(crate) state_after: ResolvedStoreDeviceState,
92    pub(crate) registrations: Vec<ActivatedStoreDeviceRegistration>,
93    pub(crate) operations: VerifiedStoreDeviceOperations,
94    pub(crate) acknowledgement: Option<(store_commit::StoreAckRef, store_commit::StoreAck)>,
95    pub(crate) membership_control: Option<VerifiedMergeMembershipControl>,
96    pub(crate) activation_head: StoreDeviceHead,
97    pub(crate) activation_head_object: ExactObjectRef,
98    pub(crate) history_evidence: store_commit::RetainedMergeCommitEvidence,
99}
100
101pub(crate) struct VerifiedMergeHistoryAuthority {
102    pub(crate) device_state: ResolvedStoreDeviceState,
103    pub(crate) membership: MembershipChain,
104}
105
106impl<'a> MergeHistoryVerifier<'a> {
107    fn cached_verified_membership(
108        &self,
109        state: &StoreMembershipStateRef,
110        authority: &VerifiedMergeMembershipPrefix,
111    ) -> Option<MembershipChain> {
112        self.verified_memberships
113            .iter()
114            .rev()
115            .find(|verified| {
116                verified.membership.head_refs() == state.heads
117                    && verified.membership.resolution_refs() == state.resolutions
118                    && authority.extends(&verified.authority)
119            })
120            .map(|verified| verified.membership.clone())
121    }
122
123    fn remember_verified_membership(
124        &mut self,
125        authority: VerifiedMergeMembershipPrefix,
126        membership: MembershipChain,
127    ) {
128        if self.verified_memberships.iter().any(|verified| {
129            verified.membership.head_refs() == membership.head_refs()
130                && verified.membership.resolution_refs() == membership.resolution_refs()
131                && authority.extends(&verified.authority)
132        }) {
133            return;
134        }
135        self.verified_memberships.push(VerifiedMembershipChain {
136            authority,
137            membership,
138        });
139    }
140
141    pub(crate) fn verified_root(&self) -> &crate::sync::store::protocol_root::VerifiedStoreRoot {
142        &self.root
143    }
144
145    pub(crate) fn membership_objects(&self) -> StoreMembershipObjectVerifier<'_, 'a> {
146        self.commit_verifier.membership_objects()
147    }
148
149    pub(crate) async fn retain_acknowledgement(
150        &self,
151        activating_commit: &StoreBatchCommitRef,
152        activating_commit_value: &StoreBatchCommit,
153        registration: &StoreDeviceRegistration,
154        reference: StoreAckRef,
155        value: StoreAck,
156    ) -> Result<store_commit::RetainedVerifiedActivatedAck, StorePullError> {
157        if activating_commit_value.acknowledgement() != Some(&reference)
158            || activating_commit_value.author_registration != reference.registration
159            || value.registration != reference.registration
160        {
161            return Err(StorePullError::InvalidState(
162                "Store acknowledgement differs from its activating commit".to_string(),
163            ));
164        }
165        activating_commit
166            .verify_commit(activating_commit_value)
167            .map_err(StorePullError::Protocol)?;
168        // Only the acknowledgement this commit activated. Its predecessors are
169        // retained beside the commits that activated them, and each
170        // acknowledgement names the object of the one before it, so the chain is
171        // walkable across rows. Walking it here and storing the result made the
172        // row grow with the history in front of it, and cost a provider read per
173        // link at the moment of applying a commit.
174        let object = value.to_bytes();
175        reference
176            .object
177            .verify(&object)
178            .map_err(|error| StorePullError::context("retained acknowledgement object", error))?;
179        StoreAck::parse_at(&object, self.root.reference(), &reference, registration)
180            .map_err(StorePullError::Protocol)?;
181        Ok(store_commit::RetainedVerifiedActivatedAck {
182            acknowledgement: (reference, value),
183            activating_commit: activating_commit.clone(),
184        })
185    }
186
187    pub(crate) async fn load_local_device_operations_with_resolver(
188        &mut self,
189        resolver: &DeviceStateResolver<'_>,
190        verified_commit: &VerifiedStoreBatchCommit,
191        membership: &MembershipChain,
192        state_ref: &StoreDeviceStateRef,
193        state: ResolvedStoreDeviceState,
194    ) -> Result<VerifiedStoreDeviceOperations, StorePullError> {
195        if verified_commit.store_root_hash() != self.root.reference().store_root_hash {
196            return Err(StorePullError::InvalidState(
197                "local device-operation commit belongs to another Store root".to_string(),
198            ));
199        }
200        let commit = verified_commit.value();
201        if commit.device_exclusion_proposals().is_empty()
202            && commit.device_exclusion_outcomes().is_empty()
203        {
204            return VerifiedStoreDeviceOperations::without_exclusions(commit)
205                .map_err(StorePullError::Protocol);
206        }
207        if state_ref != &commit.device_state {
208            return Err(StorePullError::InvalidState(
209                "local exclusion commit differs from its materialized predecessor device state"
210                    .to_string(),
211            ));
212        }
213        verify_merge_membership_state_ref(&commit.membership_state, membership, &state)?;
214        Box::pin(self.commit_verifier.load_commit_device_operations(
215            Some(resolver),
216            commit,
217            &state,
218            Some(membership),
219        ))
220        .await
221        .map_err(StorePullError::from)
222    }
223
224    pub(crate) async fn derive_local_post_device_state(
225        &self,
226        commit: &StoreBatchCommit,
227        predecessor_state: ResolvedStoreDeviceState,
228        registrations: &[ActivatedStoreDeviceRegistration],
229        device_operations: VerifiedStoreDeviceOperations,
230    ) -> Result<ResolvedStoreDeviceState, StorePullError> {
231        let (authorized_predecessor, recovery_author) = predecessor_state
232            .preactivate_recovery_author(commit, registrations)
233            .map_err(StorePullError::Protocol)?;
234        let owner_recovery = self
235            .commit_verifier
236            .verify_owner_recovery_activation(commit)
237            .await?;
238        device_operations
239            .apply_to(authorized_predecessor, &commit.device_state)
240            .and_then(|state| {
241                state.apply_verified_lifecycle(
242                    commit,
243                    registrations,
244                    recovery_author.as_ref(),
245                    owner_recovery,
246                )
247            })
248            .map_err(StorePullError::Protocol)
249    }
250
251    /// Bind a history verifier to its Store root.
252    ///
253    /// Reads the founder once, to confirm it belongs to this root and to derive
254    /// the genesis device state, then keeps only its reference. The registration
255    /// itself stays where every other one does — the commit verifier's
256    /// registration cache — so asking for it later is a lookup, not a copy held
257    /// here as well.
258    pub(crate) async fn from_commit_verifier(
259        _authority: crate::sync::store::authorization::HistoryConstructionAuthority,
260        root: crate::sync::store::protocol_root::VerifiedStoreRoot,
261        commit_verifier: StoreCommitVerifier<'a>,
262    ) -> Result<Self, StorePullError> {
263        let founder = commit_verifier.load_founder_registration().await?;
264        let founder = &founder;
265        let verified_root = root.protocol();
266        let founder_ref =
267            StoreDeviceRegistrationRef::from_registration(&founder.value, founder.object.clone());
268        let founder_origin_matches = matches!(
269            founder.value.origin,
270            store_commit::StoreDeviceRegistrationOrigin::Founder { creation_id }
271                if creation_id == verified_root.descriptor.creation_id
272        );
273        if founder.value.store_root != *root.reference()
274            || founder.value.author_pubkey != verified_root.descriptor.founder_pubkey
275            || founder.value.provider != verified_root.descriptor.founder_provider_admin.provider
276            || founder.object.slot() != &verified_root.descriptor.founder_registration
277            || founder.semantic_hash != founder_ref.registration_hash
278            || !founder_origin_matches
279        {
280            return Err(StorePullError::InvalidState(
281                "verified founder registration belongs to another Store root".to_string(),
282            ));
283        }
284        let genesis = ResolvedStoreDeviceState::founder(
285            root.reference(),
286            founder_ref.clone(),
287            &verified_root.descriptor.founder_pubkey,
288            verified_root.descriptor.founder_grant.clone(),
289            &verified_root.descriptor.founder_recovery,
290        )
291        .map_err(StorePullError::Protocol)?;
292        Ok(Self {
293            root,
294            commit_verifier,
295            founder: founder_ref,
296            history: VerifiedMergeHistory {
297                genesis,
298                baseline: coven_database::InstalledReplayBaseline::default(),
299                retained: BTreeMap::new(),
300                commits: BTreeMap::new(),
301            },
302            verified_memberships: Vec::new(),
303        })
304    }
305
306    pub(crate) async fn covered_reference_status(
307        &mut self,
308        coverage: &CommitFrontier,
309        stream_id: &str,
310        reference: &StoreBatchCommitRef,
311    ) -> MaterializedCheck {
312        if commit_stream_id(&reference.coord) != stream_id {
313            return MaterializedCheck::Held(HeldStorePositionReason::WrongSlot(format!(
314                "commit reference stream {} differs from dependency stream {stream_id}",
315                commit_stream_id(&reference.coord)
316            )));
317        }
318        let coverage = coverage.clone().into_refs();
319        let Some(covered) = coverage.get(stream_id) else {
320            return MaterializedCheck::Missing;
321        };
322        if reference.coord.sequence() > covered.coord.sequence() {
323            return MaterializedCheck::Missing;
324        }
325        let mut cursor = covered.clone();
326        loop {
327            if cursor == *reference {
328                return MaterializedCheck::Yes;
329            }
330            if cursor.coord.sequence() <= reference.coord.sequence() {
331                return MaterializedCheck::Held(HeldStorePositionReason::HashMismatch {
332                    referenced_device_id: stream_id.to_string(),
333                    referenced_commit: reference.clone(),
334                    materialized_hash: cursor.commit_hash,
335                });
336            }
337            let verified_commit = match self.load_ref(&cursor).await {
338                Ok(commit) => commit,
339                Err(error) => {
340                    return MaterializedCheck::Held(
341                        HeldStorePositionReason::ObjectUnreadablePull {
342                            key: "exact Store commit".to_string(),
343                            source: error.into(),
344                        },
345                    );
346                }
347            };
348            let Some(predecessor) = verified_commit.value().order.predecessor() else {
349                return MaterializedCheck::Missing;
350            };
351            cursor = predecessor.clone();
352        }
353    }
354
355    pub(crate) async fn validate_commit_acknowledgement(
356        &self,
357        commit: &StoreBatchCommit,
358        activating_author: &StoreDeviceRegistration,
359    ) -> Result<Option<(StoreAckRef, StoreAck)>, RegistrationLoadError> {
360        let Some(reference) = commit.acknowledgement() else {
361            return Ok(None);
362        };
363        let ack = self
364            .load_store_ack(reference, activating_author)
365            .await
366            .map_err(RegistrationLoadError::Object)?;
367        let predecessor_cut = commit
368            .order
369            .predecessor_cut()
370            .map_err(RegistrationLoadError::from)?;
371        if ack.registration != commit.author_registration
372            || ack.store_cut != predecessor_cut
373            || ack.device_state != commit.device_state
374        {
375            return Err(RegistrationLoadError::Invalid(
376                "Store acknowledgement differs from its activating commit predecessor".to_string(),
377            ));
378        }
379        if let Some(snapshot) = &ack.snapshot {
380            let snapshot_author = self
381                .load_registration(&snapshot.author_registration)
382                .await
383                .map_err(RegistrationLoadError::Object)?;
384            let (_, metadata) = self
385                .load_store_snapshot(
386                    &snapshot.author_registration,
387                    &snapshot_author.value,
388                    &snapshot.snapshot,
389                )
390                .await
391                .map_err(RegistrationLoadError::from)?;
392            if !ack.store_cut.frontier().covers(&metadata.coverage) {
393                return Err(RegistrationLoadError::Invalid(
394                    "Store acknowledgement does not cover its exact snapshot".to_string(),
395                ));
396            }
397        }
398        Ok(Some((reference.clone(), ack)))
399    }
400
401    pub(crate) fn remember(
402        &mut self,
403        commit: VerifiedStoreBatchCommit,
404    ) -> Result<(), StoreProtocolError> {
405        self.commit_verifier.remember(commit)
406    }
407
408    /// Admit the commits, announcement heads, and accepted announcement path
409    /// this device has already verified, from the retained materialization rows
410    /// that recorded them.
411    ///
412    /// The verifier's reuse memos (`commits`, `verified_heads`,
413    /// `accepted_announcements`) exist so one cycle never reads the same
414    /// protocol object twice, and they work — but they are built fresh with the
415    /// verifier, which every cycle rebuilds. Retained history therefore paid one
416    /// provider read per commit and one per activation head on every single
417    /// cycle, for commits this device verified and materialized long ago.
418    ///
419    /// The durable half of that reuse already exists one layer down: a retained
420    /// materialization row holds the commit's canonical bytes and its activation
421    /// head's, pinned by an input hash, written by the transaction that verified
422    /// and applied them, and re-parsed and signature-checked against the
423    /// activated registration every time the row is opened. This seeds the
424    /// per-cycle memos from that durable authority, so the verification below
425    /// runs unchanged and reaches the provider only for what this device has not
426    /// already verified.
427    ///
428    /// Nothing here is taken on trust: every value admitted came back through
429    /// the same signature check a provider read would have run, and the
430    /// `remember_*` entry points reject a value that disagrees with its
431    /// reference or with an entry already admitted.
432    /// Adopt the announcement position the installed Store snapshot restates
433    /// for each stream, as the point a chain walk resumes from.
434    ///
435    /// Admitted before [`admit_retained_history`](Self::admit_retained_history)
436    /// because it decides where the accepted path starts. Without it a device
437    /// whose retained rows stop above the snapshot cut has no accepted prefix
438    /// at all, and every walk falls back to the stream anchor and re-reads
439    /// every head and commit under the cut, on every pull, for as long as the
440    /// store exists.
441    ///
442    /// The authority is the one the baseline itself rests on: the owner signed
443    /// this announcement into the snapshot's history summary alongside the
444    /// state it restates, and the database refuses a frontier naming a commit
445    /// its own coverage does not.
446    pub(crate) fn admit_snapshot_announcements(
447        &mut self,
448        frontier: &BTreeMap<
449            coven_protocol::causal_grants::AuthorStreamId,
450            store_commit::RetainedAcceptedStoreAnnouncement,
451        >,
452    ) -> Result<(), StorePullError> {
453        for announcement in frontier.values() {
454            let head = &announcement.value;
455            let head_ref = StoreDeviceHeadRef {
456                head_hash: head.head_hash(),
457                object: announcement.reference.object.clone(),
458            };
459            if announcement.reference != head_ref {
460                return Err(StorePullError::InvalidState(
461                    "snapshot announcement differs from its own head reference".to_string(),
462                ));
463            }
464            self.commit_verifier
465                .remember_verified_head(
466                    &head_ref,
467                    VerifiedObject {
468                        value: head.clone(),
469                        bytes: head.to_bytes(),
470                        semantic_hash: head_ref.head_hash,
471                        object: head_ref.object.clone(),
472                    },
473                )
474                .map_err(StorePullError::Protocol)?;
475            self.commit_verifier
476                .remember_covered_announcement(
477                    &head.author_registration,
478                    crate::sync::store::commit_verification::commit::CoveredStoreAnnouncement {
479                        sequence: head.commit.coord.sequence(),
480                        commit: head.commit.clone(),
481                        head: head_ref,
482                        next_slot: head.successor.next_slot.clone(),
483                    },
484                )
485                .map_err(StorePullError::Protocol)?;
486        }
487        Ok(())
488    }
489
490    /// Adopt the replay baseline this device stands on as the floor of every
491    /// history walk this verifier runs.
492    ///
493    /// Admitted before the retained rows, for the same reason
494    /// [`admit_snapshot_announcements`](Self::admit_snapshot_announcements) is:
495    /// it decides where a walk stops, and a walk that starts before knowing
496    /// that runs to genesis over commits the baseline retired.
497    ///
498    /// Refuses to replace a baseline already admitted with a different one. One
499    /// verifier serves one operation, and a coverage that moves under it would
500    /// silently change what the walks it already ran were allowed to skip.
501    pub(crate) fn admit_installed_baseline(
502        &mut self,
503        baseline: coven_database::InstalledReplayBaseline,
504    ) -> Result<(), StorePullError> {
505        let installed = self.history.baseline.coverage();
506        if !installed.commits().is_empty() && installed != baseline.coverage() {
507            return Err(StorePullError::InvalidState(
508                "installed replay baseline coverage moved under its history verifier".to_string(),
509            ));
510        }
511        // Every acknowledgement the baseline's signed summary states, admitted
512        // before anything walks a chain. A chain walk demands contiguity from
513        // sequence one, and the acknowledgements under the coverage are exactly
514        // the rows the advance retired — so without this a device pays one
515        // provider read per acknowledgement it has ever made, every time it
516        // verifies a snapshot. The owner signed those chains into the snapshot
517        // this baseline stands on: covered positions resolve to the coverage,
518        // here as everywhere else.
519        if let Some(summary) = baseline.history_summary() {
520            for chain in summary.summary.acknowledgements.values() {
521                for (reference, value) in chain.chain.values() {
522                    self.commit_verifier
523                        .remember_acknowledgement(reference, value)
524                        .map_err(StorePullError::Protocol)?;
525                }
526            }
527        }
528        self.history.baseline = baseline;
529        Ok(())
530    }
531
532    /// The coverage this device's installed replay baseline stands at.
533    ///
534    /// A snapshot covering no more than this has nothing this device can verify
535    /// it against — the history behind it was retired — and nothing to offer
536    /// it, because the baseline restates at least as much.
537    pub(crate) fn replay_baseline_coverage(&self) -> &CommitFrontier {
538        self.history.baseline.coverage()
539    }
540
541    /// Whether this device's replay baseline was installed from `snapshot`.
542    ///
543    /// The local answer to "am I already standing on that?", which keeps a
544    /// settled cycle from reading back the snapshot it already stands on.
545    pub(crate) fn replay_baseline_stands_on(
546        &self,
547        snapshot: &store_commit::StoreSnapshotRef,
548    ) -> bool {
549        self.history.baseline.stands_on(snapshot)
550    }
551
552    /// The newest snapshot `registration` has published an acknowledgement of.
553    pub(crate) fn newest_acknowledged_snapshot(
554        &self,
555        registration: &StoreDeviceRegistrationRef,
556    ) -> Option<store_commit::StoreSnapshotLocator> {
557        self.commit_verifier
558            .newest_acknowledged_snapshot(registration)
559    }
560
561    /// The published snapshot `locator` names, read at the coordinate it names.
562    ///
563    /// The locator comes out of an acknowledgement this device published, so it
564    /// carries the snapshot's exact object and semantic hash already — there is
565    /// nothing about it left to establish by following the stream that leads to
566    /// it, and following one costs a read per generation published under it.
567    /// The object is authenticated exactly as a stream walk authenticates it,
568    /// against this Store's root, its author's registration and signature, and
569    /// the generation its own key claims.
570    ///
571    /// `None` when the provider no longer holds it: an acknowledged snapshot is
572    /// a claim about what this device stands on, not a promise that the cloud
573    /// still has it.
574    pub(crate) async fn load_acknowledged_snapshot(
575        &mut self,
576        locator: &store_commit::StoreSnapshotLocator,
577        author: &StoreDeviceRegistration,
578    ) -> Result<
579        Option<coven_database::PublishedStoreSnapshot>,
580        crate::sync::store::snapshots::SnapshotError,
581    > {
582        let (reference, meta) = match self
583            .commit_verifier
584            .load_store_snapshot(&locator.author_registration, author, &locator.snapshot)
585            .await
586        {
587            Ok(loaded) => loaded,
588            Err(StoreObjectError::Storage(StorageError::NotFound(_))) => return Ok(None),
589            Err(error) => return Err(crate::sync::store::snapshots::SnapshotError::from(error)),
590        };
591        Ok(Some(coven_database::PublishedStoreSnapshot {
592            successor_slot: meta.successor.next_slot.clone(),
593            reference,
594            meta,
595        }))
596    }
597
598    /// Adopt this device's own published snapshots as the walked prefix of its
599    /// snapshot stream, so reclaim's choice does not re-read every generation
600    /// it has ever published.
601    pub(crate) fn admit_published_snapshots(
602        &mut self,
603        snapshots: Vec<coven_database::PublishedStoreSnapshot>,
604    ) -> Result<(), StorePullError> {
605        let Some(author) = snapshots
606            .first()
607            .map(|snapshot| snapshot.meta.author_registration.clone())
608        else {
609            return Ok(());
610        };
611        if snapshots
612            .iter()
613            .any(|snapshot| snapshot.meta.author_registration != author)
614        {
615            return Err(StorePullError::InvalidState(
616                "one Store snapshot stream carries two authors".to_string(),
617            ));
618        }
619        self.commit_verifier
620            .remember_published_snapshot_stream(&author, snapshots)
621            .map_err(StorePullError::Protocol)
622    }
623
624    pub(crate) fn admit_retained_history(
625        &mut self,
626        retained: &[coven_database::OwnedVerifiedMergeMaterialization],
627    ) -> Result<(), StorePullError> {
628        let mut announced = BTreeMap::<StoreDeviceRegistrationRef, u64>::new();
629        for materialization in retained {
630            let commit = materialization.commit();
631            let commit_ref = materialization.commit_ref();
632            self.history
633                .retained
634                .insert(commit_ref.clone(), materialization.registrations().to_vec());
635            self.commit_verifier
636                .remember(materialization.verified_commit().clone())
637                .map_err(StorePullError::Protocol)?;
638            // The one acknowledgement this commit activated. Across the retained
639            // rows that is every acknowledgement the device has made, which is
640            // what the chain walk used to re-read from the provider per commit —
641            // the rows hold it between them rather than each holding all of it.
642            if let Some(activated) = &materialization.history_evidence().acknowledgement {
643                let (reference, value) = activated.acknowledgement();
644                self.commit_verifier
645                    .remember_acknowledgement(reference, value)
646                    .map_err(StorePullError::Protocol)?;
647            }
648            let head = materialization.activation_head();
649            let head_ref = StoreDeviceHeadRef {
650                head_hash: head.head_hash(),
651                object: materialization.activation_head_object().clone(),
652            };
653            self.commit_verifier
654                .remember_verified_head(
655                    &head_ref,
656                    VerifiedObject {
657                        value: head.clone(),
658                        bytes: head.to_bytes(),
659                        semantic_hash: head_ref.head_hash,
660                        object: head_ref.object.clone(),
661                    },
662                )
663                .map_err(StorePullError::Protocol)?;
664            // The accepted path is a dense sequence above the snapshot
665            // coverage, which is where `admit_snapshot_announcements` has
666            // already put its floor. Admit each stream's contiguous prefix from
667            // there and leave the rest to the discovery walk, which resumes at
668            // the first sequence the path does not cover.
669            let sequence = commit_ref.coord.sequence();
670            // The contiguous run starts one above the snapshot's coverage, not
671            // at sequence one: rows at or under the coverage are the closure
672            // the image keeps for its own reasons, not a prefix of the accepted
673            // path, and treating one of them as the start would leave the run
674            // stuck at a position the path does not hold.
675            let expected = match announced.get(&commit.author_registration) {
676                Some(previous) => previous.saturating_add(1),
677                None => self
678                    .commit_verifier
679                    .covered_announcement_floor(&commit.author_registration)
680                    .saturating_add(1),
681            };
682            if sequence != expected {
683                continue;
684            }
685            self.commit_verifier
686                .remember_accepted_announcement(
687                    &commit.author_registration,
688                    sequence,
689                    commit_ref.clone(),
690                    head_ref,
691                    head.successor.next_slot.clone(),
692                )
693                .map_err(StorePullError::Protocol)?;
694            announced.insert(commit.author_registration.clone(), sequence);
695        }
696        Ok(())
697    }
698
699    pub(crate) async fn retain_local_same_principal_join_activation(
700        &mut self,
701        materialization: coven_database::OwnedVerifiedMergeMaterialization,
702    ) -> Result<(), StorePullError> {
703        let reference = materialization.commit_ref().clone();
704        self.verify_refs(commit_predecessor_references(materialization.commit()))
705            .await?;
706        if let Some(existing) = self.history.commits.get(&reference) {
707            if existing.verified.value() == materialization.commit()
708                && existing.verified.author() == materialization.verified_commit().author()
709            {
710                return Ok(());
711            }
712            return Err(StorePullError::InvalidState(
713                "local join activation conflicts with its already-verified Store commit"
714                    .to_string(),
715            ));
716        }
717        let commit = materialization.commit();
718        if commit.control().is_some()
719            || commit.acknowledgement().is_some()
720            || commit.device_join_attempt_decisions().len() != 1
721            || commit.device_registrations().len() != 1
722            || materialization.registrations().len() != 1
723        {
724            return Err(StorePullError::InvalidState(
725                "local same-principal activation is not one exact join operation".to_string(),
726            ));
727        }
728        let predecessor_cut = commit
729            .order
730            .predecessor_cut()
731            .map_err(StorePullError::Protocol)?;
732        let authority = self.verify_merge_history_authority_from_verified_history(
733            &predecessor_cut.0,
734            &commit.membership_state,
735        )?;
736        let predecessor_state = authority.device_state;
737        let registrations = materialization.registrations().to_vec();
738        let operations = materialization.device_operations().clone();
739        let state_after = self
740            .derive_local_post_device_state(
741                commit,
742                predecessor_state.clone(),
743                &registrations,
744                operations.clone(),
745            )
746            .await?;
747        let verified = materialization.verified_commit().clone();
748        let activation_head = materialization.activation_head().clone();
749        let activation_head_object = materialization.activation_head_object().clone();
750        let history_evidence = materialization.history_evidence().clone();
751        self.history.commits.insert(
752            reference,
753            VerifiedMergeHistoryCommit {
754                verified,
755                predecessor_membership: authority.membership,
756                predecessor_state,
757                state_after,
758                registrations,
759                operations,
760                acknowledgement: None,
761                membership_control: None,
762                activation_head,
763                activation_head_object,
764                history_evidence,
765            },
766        );
767        Ok(())
768    }
769
770    pub(crate) fn verified_predecessor_state(
771        &self,
772        commit: &StoreBatchCommit,
773    ) -> Result<ResolvedStoreDeviceState, StorePullError> {
774        let states = self.history.resolved_states();
775        verified_merge_predecessor_state(&self.history.genesis, &states, commit)
776    }
777
778    pub(crate) fn verified_membership_prefix(
779        &self,
780        predecessors: impl IntoIterator<Item = StoreBatchCommitRef>,
781    ) -> Result<VerifiedMergeMembershipPrefix, StorePullError> {
782        verified_merge_membership_prefix(&self.history, predecessors)
783    }
784
785    pub(crate) fn verified_pull_candidate(
786        &self,
787        reference: &StoreBatchCommitRef,
788    ) -> Option<pull::VerifiedPullCandidate> {
789        self.history
790            .commits
791            .get(reference)
792            .map(|commit| pull::VerifiedPullCandidate {
793                verified: commit.verified.clone(),
794                predecessor_membership: commit.predecessor_membership.clone(),
795                registrations: commit.registrations.clone(),
796                operations: commit.operations.clone(),
797                membership_control: commit
798                    .membership_control
799                    .as_ref()
800                    .map(|control| control.activations.clone()),
801            })
802    }
803
804    pub(crate) fn accepted_commit_membership_state(
805        &self,
806        reference: &StoreBatchCommitRef,
807    ) -> Option<&StoreMembershipStateRef> {
808        self.history
809            .commits
810            .get(reference)
811            .map(|commit| &commit.verified.value().membership_state)
812    }
813
814    pub(crate) fn verified_predecessor_membership(
815        &self,
816        reference: &StoreBatchCommitRef,
817    ) -> Option<&MembershipChain> {
818        self.history
819            .commits
820            .get(reference)
821            .map(|commit| &commit.predecessor_membership)
822    }
823
824    pub(super) fn verifies_membership_head_activation(
825        &self,
826        reference: &protocol_membership::MembershipHeadRef,
827        head: &protocol_membership::AuthorHead,
828        activation: &StoreBatchCommitRef,
829    ) -> bool {
830        self.history
831            .commits
832            .get(activation)
833            .and_then(|commit| commit.membership_control.as_ref())
834            .is_some_and(|control| control.verifies_head_activation(reference, head, activation))
835    }
836
837    pub(super) async fn verify_membership_head_activation(
838        &mut self,
839        reference: &protocol_membership::MembershipHeadRef,
840        head: &protocol_membership::AuthorHead,
841        activation: &StoreBatchCommitRef,
842    ) -> Result<bool, StorePullError> {
843        let verified = self.load_ref(activation).await?;
844        let commit = verified.value();
845        let author = verified.author();
846        let transition = commit
847            .control()
848            .map(|control| &control.transition)
849            .ok_or_else(|| {
850                StorePullError::InvalidState(
851                    "membership head activation commit has no Merge membership transition"
852                        .to_string(),
853                )
854            })?;
855        if !transition.matches_head(head, reference)
856            || transition.body.author_registration != commit.author_registration
857        {
858            return Err(StorePullError::InvalidState(
859                "membership head differs from its exact activating Store transition".to_string(),
860            ));
861        }
862        let activation_observation = self
863            .exact_next_announcement_slot(&commit.author_registration, author, Some(&verified))
864            .await;
865        match activation_observation {
866            Ok((_, Some(_))) => {}
867            Ok((_, None)) => return Ok(false),
868            Err(StoreError::MergeAnnouncementOccupied { .. })
869            | Err(StoreError::Object(coven_protocol::objects::StoreObjectError::Storage(
870                StorageError::NotFound(_),
871            ))) => return Ok(false),
872            Err(error) => return Err(StorePullError::Store(Box::new(error))),
873        }
874        self.verify_refs([activation.clone()]).await?;
875        if !self.verifies_membership_head_activation(reference, head, activation) {
876            return Err(StorePullError::InvalidState(
877                "membership head activation differs from its verified Merge membership control"
878                    .to_string(),
879            ));
880        }
881        Ok(true)
882    }
883
884    pub(crate) async fn verify_merge_history_authority(
885        &mut self,
886        frontier: &BTreeMap<protocol_membership::AuthorStreamId, StoreBatchCommitRef>,
887        membership_state: &StoreMembershipStateRef,
888    ) -> Result<VerifiedMergeHistoryAuthority, StorePullError> {
889        self.verify_refs(frontier.values().cloned()).await?;
890        let (device_state, verified_membership_activations) =
891            self.verified_merge_history_authority_parts(frontier)?;
892        let membership = match self
893            .cached_verified_membership(membership_state, &verified_membership_activations)
894        {
895            Some(membership) => membership,
896            None => self
897                .load_membership_at_verified_prefix(
898                    &membership_state.heads,
899                    &membership_state.resolutions,
900                    &verified_membership_activations,
901                    None,
902                )
903                .await
904                .map_err(StorePullError::MembershipChain)?,
905        };
906        verified_membership_activations.validate_complete_membership(&membership)?;
907        verify_merge_membership_state_ref(membership_state, &membership, &device_state)?;
908        self.remember_verified_membership(verified_membership_activations, membership.clone());
909        Ok(VerifiedMergeHistoryAuthority {
910            device_state,
911            membership,
912        })
913    }
914
915    pub(crate) fn verify_merge_history_authority_from_verified_history(
916        &self,
917        frontier: &BTreeMap<protocol_membership::AuthorStreamId, StoreBatchCommitRef>,
918        membership_state: &StoreMembershipStateRef,
919    ) -> Result<VerifiedMergeHistoryAuthority, StorePullError> {
920        let (device_state, verified_membership_activations) =
921            self.verified_merge_history_authority_parts(frontier)?;
922        let membership = self
923            .cached_verified_membership(membership_state, &verified_membership_activations)
924            .ok_or_else(|| {
925                StorePullError::InvalidState(
926                    "Merge membership authority is absent from the already-verified history"
927                        .to_string(),
928                )
929            })?;
930        verified_membership_activations.validate_complete_membership(&membership)?;
931        verify_merge_membership_state_ref(membership_state, &membership, &device_state)?;
932        Ok(VerifiedMergeHistoryAuthority {
933            device_state,
934            membership,
935        })
936    }
937
938    fn verified_merge_history_authority_parts(
939        &self,
940        frontier: &BTreeMap<protocol_membership::AuthorStreamId, StoreBatchCommitRef>,
941    ) -> Result<(ResolvedStoreDeviceState, VerifiedMergeMembershipPrefix), StorePullError> {
942        let device_state = if frontier.is_empty() {
943            self.history.genesis.clone()
944        } else {
945            ResolvedStoreDeviceState::merge(
946                frontier
947                    .values()
948                    .map(|reference| {
949                        self.history.state_after(reference).cloned().ok_or_else(|| {
950                            StorePullError::InvalidState(
951                                "Merge history frontier is absent from its verified graph"
952                                    .to_string(),
953                            )
954                        })
955                    })
956                    .collect::<Result<Vec<_>, _>>()?,
957            )
958            .map_err(StorePullError::Protocol)?
959        };
960        let membership =
961            verified_merge_membership_prefix(&self.history, frontier.values().cloned())?;
962        Ok((device_state, membership))
963    }
964}
965
966/// Every commit `tips` causally depends on, down to the installed baseline.
967///
968/// A covered reference is a member of the closure but not a step in the walk:
969/// the baseline restates what stands there, and the commits behind it are
970/// retired. Walking past one would demand history this device deliberately
971/// dropped.
972fn verified_merge_commit_closure(
973    history: &VerifiedMergeHistory,
974    tips: impl IntoIterator<Item = StoreBatchCommitRef>,
975) -> Result<BTreeSet<StoreBatchCommitRef>, StorePullError> {
976    let mut pending = tips.into_iter().collect::<Vec<_>>();
977    let mut closure = BTreeSet::new();
978    while let Some(reference) = pending.pop() {
979        if !closure.insert(reference.clone()) {
980            continue;
981        }
982        if history.superseded(&reference) {
983            continue;
984        }
985        let verified = history.commits.get(&reference).ok_or_else(|| {
986            StorePullError::InvalidState(
987                "verified Merge predecessor closure is absent from its history".to_string(),
988            )
989        })?;
990        pending.extend(commit_predecessor_references(verified.verified.value()));
991    }
992    Ok(closure)
993}
994
995fn merge_device_state_from_verified_history(
996    reference: &StoreDeviceStateRef,
997    history: &VerifiedMergeHistory,
998    allowed_tips: impl IntoIterator<Item = StoreBatchCommitRef>,
999) -> Result<ResolvedStoreDeviceState, StorePullError> {
1000    let genesis = &history.genesis;
1001    let frontier = reference.frontier();
1002    let allowed = verified_merge_commit_closure(history, allowed_tips)?;
1003    if frontier
1004        .commits()
1005        .values()
1006        .any(|reference| !allowed.contains(reference))
1007    {
1008        return Err(StorePullError::InvalidState(
1009            "Merge device state names a commit outside its causal predecessor history".to_string(),
1010        ));
1011    }
1012    let state = if frontier.commits().is_empty() {
1013        genesis.clone()
1014    } else {
1015        ResolvedStoreDeviceState::merge(
1016            frontier
1017                .commits()
1018                .values()
1019                .map(|reference| {
1020                    history.state_after(reference).cloned().ok_or_else(|| {
1021                        StorePullError::InvalidState(
1022                            "Merge device-state frontier is absent from its verified history"
1023                                .to_string(),
1024                        )
1025                    })
1026                })
1027                .collect::<Result<Vec<_>, _>>()?,
1028        )
1029        .map_err(StorePullError::Protocol)?
1030    };
1031    let expected = StoreDeviceStateRef::from_resolved(frontier.clone(), &state)
1032        .map_err(StorePullError::Protocol)?;
1033    if &expected != reference {
1034        return Err(StorePullError::InvalidState(
1035            "Merge device-state reference differs from its verified history".to_string(),
1036        ));
1037    }
1038    Ok(state)
1039}
1040
1041pub(crate) struct VerifiedMergeHistory {
1042    pub(crate) genesis: ResolvedStoreDeviceState,
1043    /// Where a walk down this history stops, and what it reads there.
1044    ///
1045    /// Below an installed baseline there is nothing to walk to: the commits are
1046    /// retired and their rows are restated by one signed image. The two ends of
1047    /// a history are the same shape — `genesis` is the state before the first
1048    /// commit, `baseline` is the state at the positions the image covers.
1049    pub(crate) baseline: coven_database::InstalledReplayBaseline,
1050    /// The commits this device still holds a retained materialization for, and
1051    /// the registrations that row proved active at each of them.
1052    ///
1053    /// A baseline image keeps a closure of rows at or under its own coverage —
1054    /// historical Circle epoch access, author-exclusion recovery — because
1055    /// those paths read the rows rather than a replay. Being covered therefore
1056    /// does not mean the commit is gone; holding no row for it does.
1057    pub(crate) retained: BTreeMap<StoreBatchCommitRef, Vec<ActivatedStoreDeviceRegistration>>,
1058    pub(crate) commits: BTreeMap<StoreBatchCommitRef, VerifiedMergeHistoryCommit>,
1059}
1060
1061impl VerifiedMergeHistory {
1062    /// Whether the installed baseline stands in for `reference` outright: it
1063    /// restates the position and this device kept no row behind it. Nothing
1064    /// walks past such a reference, because there is nothing left to walk to.
1065    pub(crate) fn superseded(&self, reference: &StoreBatchCommitRef) -> bool {
1066        self.baseline.covers(reference) && !self.retained.contains_key(reference)
1067    }
1068
1069    /// The registrations a retained row already proved active at its commit.
1070    ///
1071    /// Re-deriving them reads whatever the commit's body names from the
1072    /// provider — a reclaim authorization, its evidence, its receipt — on every
1073    /// pull, for a commit this device verified once and wrote a row for. The
1074    /// row was written by the transaction that verified and applied the commit,
1075    /// and opening it re-parses and re-checks the commit against its activated
1076    /// registration, so it is the answer rather than a cache of one.
1077    pub(crate) fn retained_registrations(
1078        &self,
1079        reference: &StoreBatchCommitRef,
1080    ) -> Option<&[ActivatedStoreDeviceRegistration]> {
1081        self.retained
1082            .get(reference)
1083            .map(|registrations| registrations.as_slice())
1084    }
1085
1086    /// The device state standing after `reference`, from the verified graph
1087    /// when it holds the commit and from the baseline when the commit is one it
1088    /// superseded.
1089    pub(crate) fn state_after(
1090        &self,
1091        reference: &StoreBatchCommitRef,
1092    ) -> Option<&ResolvedStoreDeviceState> {
1093        self.commits
1094            .get(reference)
1095            .map(|commit| &commit.state_after)
1096            .or_else(|| self.baseline.covered_state(reference))
1097    }
1098
1099    /// Every position this history can answer a device state for: the commits
1100    /// it verified, plus the covered positions the baseline restates.
1101    pub(crate) fn resolved_states(
1102        &self,
1103    ) -> BTreeMap<StoreBatchCommitRef, ResolvedStoreDeviceState> {
1104        self.baseline
1105            .covered_states()
1106            .map(|(reference, state)| (reference.clone(), state.clone()))
1107            .chain(
1108                self.commits
1109                    .iter()
1110                    .map(|(reference, verified)| (reference.clone(), verified.state_after.clone())),
1111            )
1112            .collect()
1113    }
1114}
1115
1116struct VerifiedMembershipChain {
1117    authority: VerifiedMergeMembershipPrefix,
1118    membership: MembershipChain,
1119}
1120
1121pub struct MergeHistoryVerifier<'a> {
1122    root: crate::sync::store::protocol_root::VerifiedStoreRoot,
1123    commit_verifier: StoreCommitVerifier<'a>,
1124    /// Which registration founded this Store. Established at construction from
1125    /// the founder this verifier validated against the root; the registration it
1126    /// names is held by `commit_verifier`, not again here.
1127    founder: StoreDeviceRegistrationRef,
1128    history: VerifiedMergeHistory,
1129    verified_memberships: Vec<VerifiedMembershipChain>,
1130}
1131
1132type PredecessorCommitPredicate<'a> = Box<dyn FnMut(&VerifiedStoreBatchCommit) -> bool + Send + 'a>;
1133
1134pub struct MergeOutboundAuthorization {
1135    pub(crate) membership: MembershipChain,
1136    pub(crate) membership_state: StoreMembershipStateRef,
1137    pub(crate) device_state_ref: StoreDeviceStateRef,
1138    pub(crate) device_state: ResolvedStoreDeviceState,
1139}