Skip to main content

coven_database/store/
materialization_models.rs

1use crate::*;
2use coven_protocol::audience_package::AudiencePackage;
3use coven_protocol::circle_activation::VerifiedCircleActivations;
4use coven_protocol::membership::{
5    AuthorHead, MembershipEntry, MembershipEntryRef, MembershipHeadRef,
6};
7use coven_protocol::objects::{ExactObjectRef, PreparedExactObject};
8use coven_protocol::remote_object::{remote_object_id, SharedLiveSetObjectDomain};
9use coven_protocol::store_commit::{
10    ActivatedStoreDeviceRegistration, CirclePackageRef, ObjectHash, RetainedStoreDeviceOperations,
11    RetainedStoreDeviceRegistrationActivations, StoreBatchCommit, StoreBatchCommitRef,
12    StorePackageRef, VerifiedStoreDeviceOperations,
13};
14use coven_protocol::store_commit::{
15    RetainedMergeCommitEvidence, RetainedReplaySnapshotAuthority, StoreRootRef,
16    VerifiedStoreBatchCommit,
17};
18
19#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct RetainedMergeMaterializationInput {
22    pub commit: PreparedExactObject,
23    pub history_evidence: RetainedMergeCommitEvidence,
24    pub membership_objects: Option<VerifiedMergeMembershipObjects>,
25    pub packages: Vec<RetainedAudiencePackage>,
26    pub activation: RetainedCommitActivationInput,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct VerifiedMergeMembershipObjects {
32    entry: MembershipEntryRef,
33    head: MembershipHeadRef,
34}
35
36impl VerifiedMergeMembershipObjects {
37    pub fn entry(&self) -> &MembershipEntryRef {
38        &self.entry
39    }
40
41    pub fn head(&self) -> &MembershipHeadRef {
42        &self.head
43    }
44
45    pub fn verify(
46        commit: &StoreBatchCommit,
47        commit_ref: &StoreBatchCommitRef,
48        entry: &MembershipEntry,
49        head_value: &AuthorHead,
50        head: MembershipHeadRef,
51    ) -> Result<Self, DbError> {
52        let Some(coven_protocol::store_commit::StoreControl { transition }) = commit.control()
53        else {
54            return Err(DbError::Message(
55                "Merge membership object closure accompanies another Store control".to_string(),
56            ));
57        };
58        if transition.body.entry.coord != entry.coord()
59            || !transition.matches_head(head_value, &head)
60            || !matches!(
61                &head_value.activation,
62                coven_protocol::membership::MembershipHeadActivation::StoreCommit { commit, .. }
63                    if commit == commit_ref
64            )
65        {
66            return Err(DbError::Message(
67                "Merge membership object closure differs from its exact Store transition"
68                    .to_string(),
69            ));
70        }
71        Ok(Self {
72            entry: transition.body.entry.clone(),
73            head,
74        })
75    }
76
77    pub fn object_ids(&self) -> impl Iterator<Item = ObjectHash> + '_ {
78        [
79            remote_object_id(&self.entry.object),
80            remote_object_id(&self.head.object),
81        ]
82        .into_iter()
83    }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
87#[serde(rename_all = "snake_case", deny_unknown_fields)]
88pub enum RetainedAudiencePackage {
89    Store {
90        reference: StorePackageRef,
91        package: AudiencePackage,
92    },
93    Circle {
94        reference: CirclePackageRef,
95        package: AudiencePackage,
96    },
97}
98
99impl RetainedAudiencePackage {
100    pub fn verify(
101        commit: &StoreBatchCommit,
102        commit_ref: &StoreBatchCommitRef,
103        package: AudiencePackage,
104    ) -> Result<Self, DbError> {
105        if package.store_root_hash() != commit.store_root_hash
106            || package.write_id() != &commit.write_id
107            || package.commit_coord() != &commit_ref.coord
108            || package.candidate_family() != commit.candidate_family()
109        {
110            return Err(DbError::Message(
111                "retained audience package differs from its exact Store commit".to_string(),
112            ));
113        }
114        package
115            .validate_blob_uploader(&commit.author_registration)
116            .map_err(DbError::from)?;
117        match package.audience() {
118            coven_protocol::audience_package::PackageAudience::Store => {
119                let reference = commit.store_package().ok_or_else(|| {
120                    DbError::Message(
121                        "retained Store package is absent from its exact commit".to_string(),
122                    )
123                })?;
124                if package.schema_version() != reference.schema_version {
125                    return Err(DbError::Message(
126                        "retained Store package schema version differs from its exact commit"
127                            .to_string(),
128                    ));
129                }
130                commit
131                    .verify_store_package(&package.to_bytes())
132                    .map_err(DbError::from)?;
133                Ok(Self::Store {
134                    reference: reference.clone(),
135                    package,
136                })
137            }
138            coven_protocol::audience_package::PackageAudience::Circle {
139                circle_id,
140                control,
141                key_fingerprint,
142            } => {
143                let reference = commit
144                    .circle_packages()
145                    .iter()
146                    .find(|reference| reference.circle_id == *circle_id)
147                    .ok_or_else(|| {
148                        DbError::Message(format!(
149                            "retained Circle package {circle_id} is absent from its exact commit"
150                        ))
151                    })?;
152                if reference.control != *control
153                    || reference.key_fingerprint != *key_fingerprint
154                    || package.schema_version() != reference.package.schema_version
155                {
156                    return Err(DbError::Message(format!(
157                        "retained Circle package {circle_id} differs from its exact commit"
158                    )));
159                }
160                commit
161                    .verify_circle_package(*circle_id, &package.to_bytes())
162                    .map_err(DbError::from)?;
163                Ok(Self::Circle {
164                    reference: reference.clone(),
165                    package,
166                })
167            }
168        }
169    }
170
171    pub fn package(&self) -> &AudiencePackage {
172        match self {
173            Self::Store { package, .. } | Self::Circle { package, .. } => package,
174        }
175    }
176
177    pub fn domain(&self) -> SharedLiveSetObjectDomain {
178        match self {
179            Self::Store { reference, .. } => SharedLiveSetObjectDomain::StorePackage {
180                reference: reference.clone(),
181            },
182            Self::Circle { reference, .. } => SharedLiveSetObjectDomain::CirclePackage {
183                reference: reference.clone(),
184            },
185        }
186    }
187
188    pub fn object(&self) -> &ExactObjectRef {
189        match self {
190            Self::Store { reference, .. } => &reference.object,
191            Self::Circle { reference, .. } => &reference.package.object,
192        }
193    }
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
197#[serde(deny_unknown_fields)]
198pub struct RetainedCommitActivationInput {
199    pub registrations: RetainedStoreDeviceRegistrationActivations,
200    pub device_operations: RetainedStoreDeviceOperations,
201    pub circle_activations: Vec<u8>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub package_application: Option<RetainedPackageApplication>,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
207#[serde(rename_all = "snake_case", deny_unknown_fields)]
208pub enum RetainedPackageApplication {
209    LocallyAuthored,
210    Received { receiver_wall_ms: u64 },
211}
212
213pub struct RetainedMergeMaterializationKey {
214    pub commit_ref: String,
215    pub input_hash: ObjectHash,
216}
217
218pub struct VerifiedMergeMaterialization<'a> {
219    root: &'a coven_protocol::store_commit::StoreRootRef,
220    verified_commit: &'a coven_protocol::store_commit::VerifiedStoreBatchCommit,
221    device_operations: &'a VerifiedStoreDeviceOperations,
222    circle_activations: &'a VerifiedCircleActivations,
223    acceptance: &'a AcceptedStoreCommitEvidence,
224    history_evidence: &'a RetainedMergeCommitEvidence,
225    membership_objects: Option<&'a VerifiedMergeMembershipObjects>,
226    packages: &'a [AudiencePackage],
227    package_application: Option<RetainedPackageApplication>,
228    registrations: &'a [ActivatedStoreDeviceRegistration],
229}
230
231#[derive(Clone)]
232pub struct OwnedVerifiedMergeMaterialization {
233    root: coven_protocol::store_commit::StoreRootRef,
234    verified_commit: coven_protocol::store_commit::VerifiedStoreBatchCommit,
235    registrations: Vec<ActivatedStoreDeviceRegistration>,
236    device_operations: VerifiedStoreDeviceOperations,
237    circle_activations: VerifiedCircleActivations,
238    acceptance: AcceptedStoreCommitEvidence,
239    history_evidence: RetainedMergeCommitEvidence,
240    membership_objects: Option<VerifiedMergeMembershipObjects>,
241    packages: Vec<AudiencePackage>,
242    package_application: Option<RetainedPackageApplication>,
243    input_hash: ObjectHash,
244}
245
246/// The replay baseline a device stands on, as the history verifier needs it.
247///
248/// The snapshot owns its coverage and aggregate history state. Exact states at
249/// individual covered commits remain separate: the snapshot's aggregate state
250/// cannot answer which devices were active at an earlier commit. Snapshot
251/// retention preserves those mappings while other commits still need them.
252///
253/// Without an installed snapshot, coverage is empty and history walks reach
254/// genesis.
255#[derive(Debug, Clone, Default)]
256pub struct InstalledReplayBaseline {
257    covered_states: std::collections::BTreeMap<
258        StoreBatchCommitRef,
259        std::sync::Arc<coven_protocol::store_commit::ResolvedStoreDeviceState>,
260    >,
261    /// The snapshot this baseline was installed or advanced from, when it came
262    /// from one. Keeping the exact published snapshot lets pre-activation join
263    /// verification read its installed starting point without requiring a
264    /// local author registration.
265    snapshot: Option<PublishedStoreSnapshot>,
266}
267
268impl InstalledReplayBaseline {
269    pub fn from_snapshot(
270        snapshot: PublishedStoreSnapshot,
271        covered_states: std::collections::BTreeMap<
272            StoreBatchCommitRef,
273            std::sync::Arc<coven_protocol::store_commit::ResolvedStoreDeviceState>,
274        >,
275    ) -> Self {
276        Self {
277            covered_states,
278            snapshot: Some(snapshot),
279        }
280    }
281
282    /// Whether this baseline was installed from `snapshot` itself.
283    pub fn stands_on(&self, snapshot: &coven_protocol::store_commit::StoreSnapshotRef) -> bool {
284        self.snapshot
285            .as_ref()
286            .is_some_and(|installed| &installed.reference == snapshot)
287    }
288
289    /// The exact snapshot installed as this replay's starting point.
290    pub fn snapshot(&self) -> Option<&PublishedStoreSnapshot> {
291        self.snapshot.as_ref()
292    }
293
294    pub fn coverage(&self) -> &coven_protocol::store_commit::CommitFrontier {
295        match &self.snapshot {
296            Some(snapshot) => &snapshot.meta.coverage,
297            None => {
298                static EMPTY: coven_protocol::store_commit::CommitFrontier =
299                    coven_protocol::store_commit::CommitFrontier(std::collections::BTreeMap::new());
300                &EMPTY
301            }
302        }
303    }
304
305    /// Whether the baseline restates `reference`, so no walk need pass it.
306    pub fn covers(&self, reference: &StoreBatchCommitRef) -> bool {
307        self.coverage().covers_commit(reference)
308    }
309
310    /// The device state that stood at a covered position, or `None` when this
311    /// device never recorded one there — which is a commit naming a position
312    /// outside its own history, not a baseline that lost something.
313    pub fn covered_state(
314        &self,
315        reference: &StoreBatchCommitRef,
316    ) -> Option<&coven_protocol::store_commit::ResolvedStoreDeviceState> {
317        self.covered_states.get(reference).map(AsRef::as_ref)
318    }
319
320    pub fn covered_states(
321        &self,
322    ) -> impl Iterator<
323        Item = (
324            &StoreBatchCommitRef,
325            &coven_protocol::store_commit::ResolvedStoreDeviceState,
326        ),
327    > {
328        self.covered_states
329            .iter()
330            .map(|(reference, state)| (reference, state.as_ref()))
331    }
332}
333
334pub enum RetainedMergeHistoryCheckpoint {
335    Snapshot(coven_protocol::store_commit::OpenedRetainedMergeHistorySummary),
336    Commit(Box<OwnedVerifiedMergeMaterialization>),
337}
338
339impl OwnedVerifiedMergeMaterialization {
340    pub fn verify(
341        root: coven_protocol::store_commit::StoreRootRef,
342        verified_commit: coven_protocol::store_commit::VerifiedStoreBatchCommit,
343        registrations: Vec<ActivatedStoreDeviceRegistration>,
344        device_operations: VerifiedStoreDeviceOperations,
345        circle_activations: VerifiedCircleActivations,
346        acceptance: AcceptedStoreCommitEvidence,
347        history_evidence: RetainedMergeCommitEvidence,
348        membership_objects: Option<VerifiedMergeMembershipObjects>,
349        packages: Vec<AudiencePackage>,
350        package_application: Option<RetainedPackageApplication>,
351        input_hash: ObjectHash,
352    ) -> Result<Self, DbError> {
353        VerifiedMergeMaterialization::verify(
354            &root,
355            &verified_commit,
356            &registrations,
357            &device_operations,
358            &circle_activations,
359            &acceptance,
360            &history_evidence,
361            membership_objects.as_ref(),
362            &packages,
363            package_application,
364        )?;
365        Ok(Self {
366            root,
367            verified_commit,
368            registrations,
369            device_operations,
370            circle_activations,
371            acceptance,
372            history_evidence,
373            membership_objects,
374            packages,
375            package_application,
376            input_hash,
377        })
378    }
379
380    pub fn input_hash(&self) -> ObjectHash {
381        self.input_hash
382    }
383
384    pub fn root(&self) -> &coven_protocol::store_commit::StoreRootRef {
385        &self.root
386    }
387
388    pub fn commit(&self) -> &StoreBatchCommit {
389        self.verified_commit.value()
390    }
391
392    pub fn commit_ref(&self) -> &StoreBatchCommitRef {
393        self.verified_commit.reference()
394    }
395
396    pub fn verified_commit(&self) -> &coven_protocol::store_commit::VerifiedStoreBatchCommit {
397        &self.verified_commit
398    }
399
400    pub fn registrations(&self) -> &[ActivatedStoreDeviceRegistration] {
401        &self.registrations
402    }
403
404    pub fn device_operations(&self) -> &VerifiedStoreDeviceOperations {
405        &self.device_operations
406    }
407
408    pub fn circle_activations(&self) -> &VerifiedCircleActivations {
409        &self.circle_activations
410    }
411
412    pub fn circle_activation(
413        &self,
414        circle_id: coven_protocol::circle::CircleId,
415        control: &coven_protocol::circle::CircleControlCoord,
416    ) -> Result<coven_protocol::circle_activation::VerifiedCircleReference, DbError> {
417        let mut matches = self
418            .circle_activations
419            .circles()
420            .iter()
421            .filter(|activation| {
422                activation.circle_id == circle_id && activation.control.coord == *control
423            });
424        let activation = matches.next().cloned().ok_or_else(|| {
425            DbError::Message(format!(
426                "Circle {circle_id} retained activation omits control {control:?}"
427            ))
428        })?;
429        if matches.next().is_some() {
430            return Err(DbError::Message(format!(
431                "Circle {circle_id} retained activation duplicates control {control:?}"
432            )));
433        }
434        Ok(activation)
435    }
436
437    pub fn acceptance(&self) -> &AcceptedStoreCommitEvidence {
438        &self.acceptance
439    }
440
441    pub fn history_evidence(&self) -> &RetainedMergeCommitEvidence {
442        &self.history_evidence
443    }
444
445    pub fn membership_objects(&self) -> Option<&VerifiedMergeMembershipObjects> {
446        self.membership_objects.as_ref()
447    }
448
449    pub(crate) fn membership_remote_objects(
450        &self,
451    ) -> Result<Vec<coven_protocol::remote_object::ClosedRemoteObject>, DbError> {
452        let Some(objects) = self.membership_objects() else {
453            return Ok(Vec::new());
454        };
455        let proof = self
456            .history_evidence
457            .membership_proof
458            .as_ref()
459            .expect("verified membership objects have their exact retained proof");
460        // Verification binds these canonical plaintext bytes to the exact
461        // objects. A retained image need not carry a second remote-record copy.
462        let entry = serde_json::to_vec(&proof.entry_value)?;
463        let head = serde_json::to_vec(&proof.head_value)?;
464        activated_merge_membership_remote_objects(
465            self.commit().candidate_family(),
466            objects,
467            MembershipAuthorityBytes::new(entry.clone(), entry),
468            MembershipAuthorityBytes::new(head.clone(), head),
469            self.commit_ref(),
470        )
471        .map_err(DbError::from)
472    }
473
474    pub fn packages(&self) -> &[AudiencePackage] {
475        &self.packages
476    }
477
478    pub fn package_application(&self) -> Option<RetainedPackageApplication> {
479        self.package_application
480    }
481}
482
483impl<'a> VerifiedMergeMaterialization<'a> {
484    pub fn root(&self) -> &coven_protocol::store_commit::StoreRootRef {
485        self.root
486    }
487
488    pub fn commit(&self) -> &StoreBatchCommit {
489        self.verified_commit.value()
490    }
491
492    pub fn commit_ref(&self) -> &StoreBatchCommitRef {
493        self.verified_commit.reference()
494    }
495
496    pub fn verified_commit(&self) -> &coven_protocol::store_commit::VerifiedStoreBatchCommit {
497        self.verified_commit
498    }
499
500    pub fn registrations(&self) -> &[ActivatedStoreDeviceRegistration] {
501        self.registrations
502    }
503
504    pub fn device_operations(&self) -> &VerifiedStoreDeviceOperations {
505        self.device_operations
506    }
507
508    pub fn circle_activations(&self) -> &VerifiedCircleActivations {
509        self.circle_activations
510    }
511
512    pub fn acceptance(&self) -> &AcceptedStoreCommitEvidence {
513        self.acceptance
514    }
515
516    pub fn history_evidence(&self) -> &RetainedMergeCommitEvidence {
517        self.history_evidence
518    }
519
520    pub fn membership_objects(&self) -> Option<&VerifiedMergeMembershipObjects> {
521        self.membership_objects
522    }
523
524    pub fn packages(&self) -> &[AudiencePackage] {
525        self.packages
526    }
527
528    pub fn package_application(&self) -> Option<RetainedPackageApplication> {
529        self.package_application
530    }
531
532    pub fn verify(
533        root: &'a coven_protocol::store_commit::StoreRootRef,
534        verified_commit: &'a coven_protocol::store_commit::VerifiedStoreBatchCommit,
535        registrations: &'a [ActivatedStoreDeviceRegistration],
536        device_operations: &'a VerifiedStoreDeviceOperations,
537        circle_activations: &'a VerifiedCircleActivations,
538        acceptance: &'a AcceptedStoreCommitEvidence,
539        history_evidence: &'a RetainedMergeCommitEvidence,
540        membership_objects: Option<&'a VerifiedMergeMembershipObjects>,
541        packages: &'a [AudiencePackage],
542        package_application: Option<RetainedPackageApplication>,
543    ) -> Result<Self, DbError> {
544        let commit = verified_commit.value();
545        let commit_ref = verified_commit.reference();
546        history_evidence
547            .validate_for(commit_ref, commit)
548            .map_err(DbError::from)?;
549        if verified_commit.store_root_hash() != root.store_root_hash
550            || acceptance.commit_ref() != commit_ref
551            || circle_activations.stream_activations().activating_commit() != commit_ref
552            || circle_activations.stream_activations().as_slice() != commit.stream_activations()
553            || circle_activations.circles().len() != commit.circle_controls().len()
554            || circle_activations
555                .circles()
556                .iter()
557                .zip(commit.circle_controls())
558                .any(|(activation, reference)| activation.reference != *reference)
559            || packages.is_empty() != package_application.is_none()
560            || commit.control().is_some() != membership_objects.is_some()
561        {
562            return Err(DbError::Message(
563                "verified Merge materialization differs from its exact Store commit".to_string(),
564            ));
565        }
566        let retained_objects = history_evidence
567            .membership_proof
568            .as_ref()
569            .map(|proof| {
570                VerifiedMergeMembershipObjects::verify(
571                    commit,
572                    commit_ref,
573                    &proof.entry_value,
574                    &proof.head_value,
575                    proof.head.clone(),
576                )
577            })
578            .transpose()?;
579        if membership_objects != retained_objects.as_ref() {
580            return Err(DbError::Message(
581                "Merge membership objects differ from their retained exact proof".into(),
582            ));
583        }
584        RetainedStoreDeviceRegistrationActivations::from_verified(root, commit, registrations)
585            .map_err(DbError::from)?;
586        Ok(Self {
587            root,
588            verified_commit,
589            device_operations,
590            circle_activations,
591            acceptance,
592            history_evidence,
593            membership_objects,
594            packages,
595            package_application,
596            registrations,
597        })
598    }
599}
600
601pub struct PreparedMergeMaterializationPackage {
602    pub package: AudiencePackage,
603    pub changeset: ValidatedChangeset<Vec<u8>>,
604}
605
606pub struct PreparedMergeMaterialization {
607    pub root: StoreRootRef,
608    pub verified_commit: VerifiedStoreBatchCommit,
609    pub acceptance: AcceptedStoreCommitEvidence,
610    pub history_evidence: RetainedMergeCommitEvidence,
611    pub membership_objects: Option<VerifiedMergeMembershipObjects>,
612    pub membership_remote_objects: Vec<coven_protocol::remote_object::ClosedRemoteObject>,
613    pub registrations: Vec<ActivatedStoreDeviceRegistration>,
614    pub packages: Vec<PreparedMergeMaterializationPackage>,
615    pub device_operations: VerifiedStoreDeviceOperations,
616    pub circle_activations: VerifiedCircleActivations,
617    pub package_application: Option<crate::RetainedPackageApplication>,
618}
619
620pub struct MembershipAuthorityBytes {
621    canonical: Vec<u8>,
622    stored: Vec<u8>,
623}
624
625impl MembershipAuthorityBytes {
626    pub fn new(canonical: Vec<u8>, stored: Vec<u8>) -> Self {
627        Self { canonical, stored }
628    }
629}
630
631pub fn activated_merge_membership_remote_objects(
632    family: coven_protocol::store_commit::CandidateFamilyId,
633    objects: &VerifiedMergeMembershipObjects,
634    entry_bytes: MembershipAuthorityBytes,
635    head_bytes: MembershipAuthorityBytes,
636    commit_ref: &StoreBatchCommitRef,
637) -> Result<
638    Vec<coven_protocol::remote_object::ClosedRemoteObject>,
639    coven_protocol::remote_object::RemoteObjectRecordError,
640> {
641    let remotes = vec![
642        coven_protocol::remote_object::RemoteObjectRecord::candidate_exclusive_merge_membership_entry(
643            family,
644            objects.entry().clone(),
645            &entry_bytes.canonical,
646            &entry_bytes.stored,
647            commit_ref.clone(),
648        )?
649        .map_record(|record| record.into_observed_activated(commit_ref))?,
650        coven_protocol::remote_object::RemoteObjectRecord::candidate_exclusive_merge_membership_head(
651            family,
652            objects.head().clone(),
653            &head_bytes.canonical,
654            &head_bytes.stored,
655            commit_ref.clone(),
656        )?
657        .map_record(|record| record.into_observed_activated(commit_ref))?,
658    ];
659    Ok(remotes)
660}
661
662/// One snapshot verified as installable: its signed metadata, the cut it
663/// covers, and the devices and registrations active there. A device installs
664/// its baseline from this and verifies everything that arrives afterwards
665/// against it.
666#[derive(Debug)]
667pub struct VerifiedStoreSnapshotAuthority {
668    authority: RetainedReplaySnapshotAuthority,
669}
670
671impl VerifiedStoreSnapshotAuthority {
672    pub fn from_authority(
673        authority: RetainedReplaySnapshotAuthority,
674    ) -> Result<Self, crate::DbError> {
675        authority.validate()?;
676        Ok(Self { authority })
677    }
678
679    pub fn into_authority(self) -> RetainedReplaySnapshotAuthority {
680        self.authority
681    }
682}
683
684#[derive(Clone)]
685pub struct DeviceJoinBootstrapCommit {
686    pub reference: StoreBatchCommitRef,
687    pub commit: VerifiedStoreBatchCommit,
688    pub registrations: Vec<ActivatedStoreDeviceRegistration>,
689    pub device_operations: VerifiedStoreDeviceOperations,
690    pub history_evidence: RetainedMergeCommitEvidence,
691}
692
693pub struct DeviceJoinBootstrapPlan {
694    pub founder_reference: StoreDeviceRegistrationRef,
695    pub founder: StoreDeviceRegistration,
696    pub founder_bytes: Vec<u8>,
697    pub genesis: ResolvedStoreDeviceState,
698    pub membership: InitialStoreMembershipAuthority,
699    pub publication: AcceptedStorePublicationInterval,
700    pub commits: Vec<DeviceJoinBootstrapCommit>,
701}
702
703/// Everything one bootstrap commit needs to materialize its rows, for a commit
704/// the joining database does not already cover through an installed snapshot.
705///
706/// Installation runs inside a single database transaction and cannot read the
707/// cloud, so the joining device resolves this beforehand — reading, decrypting
708/// and verifying each package exactly the way an ordinary pull does.
709pub struct DeviceJoinBootstrapRowData {
710    pub circle_activations: VerifiedCircleActivations,
711    pub membership_objects: Option<VerifiedMergeMembershipObjects>,
712    pub membership_remote_objects: Vec<coven_protocol::remote_object::ClosedRemoteObject>,
713    pub packages: Vec<PreparedMergeMaterializationPackage>,
714}
715
716/// A bootstrap plan together with the row data for every commit in it the
717/// joining database does not already materialize. Installation only accepts
718/// this shape, so a bootstrap can never advance its position over commits
719/// whose rows were never resolved.
720pub struct ResolvedDeviceJoinBootstrap {
721    pub plan: DeviceJoinBootstrapPlan,
722    pub snapshot_circles: crate::StagedCircleRestore,
723    pub row_data: std::collections::BTreeMap<StoreBatchCommitRef, DeviceJoinBootstrapRowData>,
724    pub local_store_membership: coven_protocol::membership::LocalStoreMembership,
725    pub routing_key: Option<coven_protocol::circle::RowRoutingKey>,
726    pub receiver_wall_ms: u64,
727}
728
729#[path = "device_join_bootstrap.rs"]
730mod device_join_bootstrap;