Skip to main content

coven_protocol/store_commit/
retained_history.rs

1use super::validation::require_version;
2use super::*;
3
4#[path = "pending_device_join.rs"]
5mod pending_device_join;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(deny_unknown_fields)]
9pub struct MembershipCausalFloor {
10    pub effective_coordinates: Vec<MembershipCoord>,
11}
12
13impl MembershipCausalFloor {
14    pub fn from_membership(membership: &crate::membership::MembershipChain) -> Self {
15        Self {
16            effective_coordinates: membership.effective_frontier(),
17        }
18    }
19
20    pub fn advance(
21        &mut self,
22        coordinate: crate::membership::MembershipCoord,
23    ) -> Result<(), StoreProtocolError> {
24        let stream = coordinate.stream_key();
25        self.effective_coordinates
26            .retain(|current| current.stream_key() != stream);
27        self.effective_coordinates.push(coordinate);
28        self.effective_coordinates.sort();
29        self.validate()
30    }
31
32    pub fn is_included_in(&self, membership: &crate::membership::MembershipChain) -> bool {
33        self.effective_coordinates
34            .iter()
35            .all(|coordinate| membership.effectively_contains_coord(coordinate))
36    }
37
38    fn validate(&self) -> Result<(), StoreProtocolError> {
39        if self
40            .effective_coordinates
41            .windows(2)
42            .any(|pair| pair[0] >= pair[1])
43        {
44            return Err(StoreProtocolError::Malformed(
45                "Merge history membership floor is not canonical".to_string(),
46            ));
47        }
48        Ok(())
49    }
50}
51
52/// The acknowledgement one commit activated, together with any uploaded
53/// predecessors whose activation candidates this operation retired. Previously
54/// activated acknowledgements remain owned by their own retained commits.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(deny_unknown_fields)]
57pub struct RetainedVerifiedActivatedAck {
58    pub acknowledgement: (StoreAckRef, StoreAck),
59    pub activating_commit: StoreBatchCommitRef,
60    pub predecessors: Vec<(StoreAckRef, StoreAck)>,
61}
62
63/// A device's acknowledgement chain, contiguous from sequence one, carried by a
64/// snapshot's portable summary.
65///
66/// This is the one place the whole chain belongs. A device restoring from a
67/// snapshot has no retained rows to walk, so the summary has to state the
68/// contiguity itself; it is folded once per snapshot from the rows
69/// the snapshot covers, rather than rebuilt into every row.
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(deny_unknown_fields)]
72pub struct RetainedAcknowledgementChain {
73    #[serde(with = "ordered_map_entries")]
74    pub chain: BTreeMap<u64, (StoreAckRef, StoreAck)>,
75    pub activating_commit: StoreBatchCommitRef,
76    pub activating_commit_value: StoreBatchCommit,
77}
78
79/// Everything a device needs to install one snapshot as its starting state and
80/// verify what arrives after it: the Store root and founder it belongs to, the
81/// signed metadata, the cut it covers, and the device state and registrations
82/// active at that cut.
83///
84/// Every field is re-derived from the signed `metadata` by
85/// [`validate`](Self::validate), so an installing device trusts the owner's
86/// signature over the snapshot and nothing local. What is deliberately absent
87/// is any claim about the *other* devices having caught up: that is
88/// a separate access concern. A device installing
89/// a baseline verifies each later commit against the registrations and device
90/// state carried here, exactly as a device that never installed a snapshot
91/// verifies them against its own history.
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(deny_unknown_fields)]
94pub struct RetainedReplaySnapshotAuthority {
95    pub store_root: StoreRootRef,
96    pub founder_registration: StoreDeviceRegistrationRef,
97    pub snapshot: StoreSnapshotRef,
98    pub metadata: SnapshotMeta,
99    #[serde(with = "ordered_map_entries")]
100    pub active_registrations: BTreeMap<StoreDeviceId, ReferencedStoreDeviceRegistration>,
101}
102
103impl RetainedReplaySnapshotAuthority {
104    pub fn validate(&self) -> Result<(), StoreProtocolError> {
105        self.metadata.state.devices.validate_canonical()?;
106        let author = self
107            .active_registrations
108            .get(&self.metadata.author_registration.device_id)
109            .filter(|registration| registration.reference() == &self.metadata.author_registration)
110            .ok_or_else(|| {
111                StoreProtocolError::Malformed(
112                    "retained snapshot author is absent from its active registrations".to_string(),
113                )
114            })?;
115        self.metadata.verify_at(
116            self.store_root.store_root_hash,
117            &self.snapshot,
118            author.value(),
119        )?;
120        let expected_active = self
121            .metadata
122            .state
123            .devices
124            .devices
125            .iter()
126            .filter_map(|(device_id, record)| {
127                matches!(record.status, StoreDeviceStatus::Active)
128                    .then_some((*device_id, &record.registration))
129            })
130            .collect::<BTreeMap<_, _>>();
131        if expected_active.len() != self.active_registrations.len()
132            || expected_active.iter().any(|(device_id, reference)| {
133                self.active_registrations
134                    .get(device_id)
135                    .is_none_or(|registration| registration.reference() != *reference)
136            })
137        {
138            return Err(StoreProtocolError::Malformed(
139                "retained snapshot replay authority does not exactly cover active devices"
140                    .to_string(),
141            ));
142        }
143        for (device_id, registration) in &self.active_registrations {
144            let bytes = registration.value().to_bytes();
145            registration.reference().object.verify(&bytes)?;
146            StoreDeviceRegistration::parse_at(&bytes, &self.store_root, *device_id)?;
147            registration
148                .reference()
149                .verify_registration(registration.value())?;
150        }
151        Ok(())
152    }
153}
154
155impl RetainedVerifiedActivatedAck {
156    pub fn acknowledgement(&self) -> &(StoreAckRef, StoreAck) {
157        &self.acknowledgement
158    }
159
160    pub fn proof_objects(&self) -> impl Iterator<Item = &(StoreAckRef, StoreAck)> {
161        self.predecessors
162            .iter()
163            .chain(std::iter::once(&self.acknowledgement))
164    }
165
166    pub fn validate_predecessors(&self) -> Result<(), StoreProtocolError> {
167        let mut successor = &self.acknowledgement;
168        for predecessor in self.predecessors.iter().rev() {
169            let (reference, value) = predecessor;
170            reference.object.verify(&value.to_bytes())?;
171            if reference.registration != self.acknowledgement.0.registration
172                || reference.registration != value.registration
173                || reference.sequence != value.sequence
174                || reference.ack_hash != value.ack_hash()
175                || reference.sequence.checked_add(1) != Some(successor.0.sequence)
176                || successor.1.successor.predecessor.as_ref() != Some(&reference.object)
177                || successor.0.object.slot() != &value.successor.next_slot
178                || value.successor.activation != successor.1.successor.activation
179                || value.store_root_hash != successor.1.store_root_hash
180            {
181                return Err(StoreProtocolError::DeviceStateMismatch);
182            }
183            successor = predecessor;
184        }
185        Ok(())
186    }
187}
188
189impl RetainedAcknowledgementChain {
190    /// Start a chain from the one acknowledgement a commit activated. Contiguity
191    /// is not claimed yet: [`extend`](Self::extend) adds the rest, and
192    /// [`validate_chain`](Self::validate_chain) is what asserts the result runs
193    /// from sequence one.
194    pub fn activated(
195        activated: &RetainedVerifiedActivatedAck,
196        activating_commit_value: &StoreBatchCommit,
197    ) -> Self {
198        Self {
199            chain: activated
200                .proof_objects()
201                .map(|proof| (proof.0.sequence, proof.clone()))
202                .collect(),
203            activating_commit: activated.activating_commit.clone(),
204            activating_commit_value: activating_commit_value.clone(),
205        }
206    }
207
208    /// Fold one more retained acknowledgement in. A sequence already present
209    /// must carry the same acknowledgement — two different ones at one sequence
210    /// is a forked chain, not a longer one. The activating commit tracks the
211    /// highest sequence, which is the one the summary reports.
212    pub fn extend(
213        &mut self,
214        activated: &RetainedVerifiedActivatedAck,
215        activating_commit_value: &StoreBatchCommit,
216    ) -> bool {
217        let (reference, _) = &activated.acknowledgement;
218        for proof in activated.proof_objects() {
219            match self.chain.get(&proof.0.sequence) {
220                Some(existing) if existing == proof => {}
221                Some(_) => return false,
222                None => {
223                    self.chain.insert(proof.0.sequence, proof.clone());
224                }
225            }
226        }
227        if self
228            .latest()
229            .is_some_and(|(latest, _)| latest.sequence == reference.sequence)
230        {
231            self.activating_commit = activated.activating_commit.clone();
232            self.activating_commit_value = activating_commit_value.clone();
233        }
234        true
235    }
236
237    pub fn latest(&self) -> Option<&(StoreAckRef, StoreAck)> {
238        self.chain
239            .last_key_value()
240            .map(|(_, acknowledgement)| acknowledgement)
241    }
242
243    pub fn exactly_extends(&self, predecessor: &Self) -> bool {
244        self.chain.len() > predecessor.chain.len()
245            && predecessor.chain.iter().all(|(sequence, acknowledgement)| {
246                self.chain.get(sequence) == Some(acknowledgement)
247            })
248    }
249
250    pub fn validate_chain(
251        &self,
252        root: &StoreRootRef,
253        registration: &ReferencedStoreDeviceRegistration,
254    ) -> Result<(), StoreProtocolError> {
255        if self.chain.is_empty() {
256            return Err(StoreProtocolError::DeviceStateMismatch);
257        }
258        let mut predecessor: Option<&StoreAckRef> = None;
259        for (expected_sequence, (sequence, (reference, value))) in (1_u64..).zip(self.chain.iter())
260        {
261            if *sequence != expected_sequence
262                || reference.sequence != expected_sequence
263                || value.sequence != expected_sequence
264                || reference.registration != *registration.reference()
265                || value.registration != *registration.reference()
266                || value.successor.predecessor.as_ref()
267                    != predecessor.map(|reference| &reference.object)
268            {
269                return Err(StoreProtocolError::DeviceStateMismatch);
270            }
271            reference.object.verify(&value.to_bytes())?;
272            StoreAck::parse_at(&value.to_bytes(), root, reference, registration.value())?;
273            predecessor = Some(reference);
274        }
275        Ok(())
276    }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(deny_unknown_fields)]
281pub struct RetainedMergeMembershipProof {
282    pub commit: StoreBatchCommitRef,
283    pub commit_value: StoreBatchCommit,
284    pub entry: MembershipEntryRef,
285    pub entry_value: MembershipEntry,
286    pub head: MembershipHeadRef,
287    pub head_value: AuthorHead,
288}
289
290/// The proof values introduced by one verified Merge commit and retained with
291/// that commit after its remote authority objects can be reclaimed.
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(deny_unknown_fields)]
294pub struct RetainedMergeCommitEvidence {
295    pub acknowledgement: Option<Box<RetainedVerifiedActivatedAck>>,
296    pub membership_proof: Option<Box<RetainedMergeMembershipProof>>,
297}
298
299impl RetainedMergeCommitEvidence {
300    pub fn none() -> Self {
301        Self {
302            acknowledgement: None,
303            membership_proof: None,
304        }
305    }
306
307    pub fn validate_for(
308        &self,
309        commit_ref: &StoreBatchCommitRef,
310        commit: &StoreBatchCommit,
311    ) -> Result<(), StoreProtocolError> {
312        commit_ref.verify_commit(commit)?;
313        if commit.acknowledgement().is_some() != self.acknowledgement.is_some()
314            || commit.control().is_some() != self.membership_proof.is_some()
315        {
316            return Err(StoreProtocolError::DeviceStateMismatch);
317        }
318        if let Some(acknowledgement) = &self.acknowledgement {
319            acknowledgement.validate_predecessors()?;
320            let (reference, _) = acknowledgement.acknowledgement();
321            if acknowledgement.activating_commit != *commit_ref
322                || commit.acknowledgement() != Some(reference)
323            {
324                return Err(StoreProtocolError::DeviceStateMismatch);
325            }
326        }
327        if let Some(proof) = &self.membership_proof {
328            if proof.commit != *commit_ref || proof.commit_value != *commit {
329                return Err(StoreProtocolError::DeviceStateMismatch);
330            }
331            let control = commit
332                .control()
333                .ok_or(StoreProtocolError::DeviceStateMismatch)?;
334            if control.transition.body.entry != proof.entry
335                || proof.entry.coord != proof.entry_value.coord()
336                || !crate::membership::verify_membership_entry(&proof.entry_value)
337                || !control
338                    .transition
339                    .matches_head(&proof.head_value, &proof.head)
340                || !matches!(
341                    &proof.head_value.activation,
342                    crate::membership::MembershipHeadActivation::StoreCommit { commit, .. }
343                        if commit == commit_ref
344                )
345            {
346                return Err(StoreProtocolError::DeviceStateMismatch);
347            }
348            proof
349                .entry
350                .object
351                .verify(&serde_json::to_vec(&proof.entry_value)?)?;
352            proof
353                .head
354                .object
355                .verify(&serde_json::to_vec(&proof.head_value)?)?;
356        }
357        Ok(())
358    }
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362#[serde(deny_unknown_fields)]
363pub struct RetainedVerifiedMergeHistorySummary {
364    pub reclaim: RetainedReclaimState,
365    pub version: u32,
366    pub store_root_hash: ObjectHash,
367    #[serde(with = "ordered_map_entries")]
368    pub causal_cut: BTreeMap<StoreCommitCoord, StoreBatchCommitRef>,
369    /// Latest meaningful publication in each author stream at this snapshot's
370    /// frontier. Canonical composition derives these exact references from
371    /// verified commits; they classify acknowledgement-only advances without
372    /// retaining those commits or authenticating arbitrary historical cuts.
373    #[serde(with = "ordered_map_entries")]
374    pub last_non_acknowledgement_commits: BTreeMap<AuthorStreamId, StoreBatchCommitRef>,
375    pub post_state: StoreDeviceStateRef,
376    pub membership_floor: MembershipCausalFloor,
377    #[serde(with = "ordered_map_entries")]
378    pub registrations: BTreeMap<StoreDeviceId, ReferencedStoreDeviceRegistration>,
379    #[serde(with = "ordered_map_entries")]
380    pub acknowledgements: BTreeMap<StoreDeviceId, RetainedAcknowledgementChain>,
381    #[serde(with = "ordered_map_entries")]
382    pub membership_proofs: BTreeMap<StoreBatchCommitRef, RetainedMergeMembershipProof>,
383    #[serde(with = "ordered_map_entries")]
384    pub pending_owner_promotions: BTreeMap<OwnerPromotionId, RetainedOwnerPromotionRequest>,
385    #[serde(with = "ordered_map_entries")]
386    pub pending_device_joins:
387        BTreeMap<StoreBatchCommitRef, device_join_exchange::DeviceJoinBootstrapClosure>,
388}
389
390#[derive(Debug, Clone)]
391pub struct OpenedRetainedMergeHistorySummary {
392    pub summary: RetainedVerifiedMergeHistorySummary,
393    pub post_state: ResolvedStoreDeviceState,
394}
395
396impl RetainedVerifiedMergeHistorySummary {
397    pub fn validate_shape(&self) -> Result<(), StoreProtocolError> {
398        require_version(self.version)?;
399        self.reclaim.validate()?;
400        self.membership_floor.validate()?;
401        let mut frontier = BTreeMap::new();
402        for (coord, reference) in &self.causal_cut {
403            if coord != &reference.coord {
404                return Err(StoreProtocolError::Malformed(
405                    "Merge history causal cut contains a mismatched coordinate".to_string(),
406                ));
407            }
408            let stream_id = reference.coord.stream_id;
409            let sequence = reference.coord.sequence;
410            match frontier.entry(stream_id) {
411                std::collections::btree_map::Entry::Vacant(entry) => {
412                    entry.insert(reference.clone());
413                }
414                std::collections::btree_map::Entry::Occupied(mut entry) => {
415                    if sequence > entry.get().coord.sequence() {
416                        entry.insert(reference.clone());
417                    }
418                }
419            }
420        }
421        if self.post_state.frontier() != &CommitFrontier(frontier) {
422            return Err(StoreProtocolError::DeviceStateMismatch);
423        }
424        for (stream, reference) in &self.last_non_acknowledgement_commits {
425            reference.coord.validate()?;
426            if stream != &reference.coord.stream_id
427                || !self.post_state.frontier().covers_commit(reference)
428                || self
429                    .causal_cut
430                    .get(&reference.coord)
431                    .is_some_and(|covered| covered != reference)
432            {
433                return Err(StoreProtocolError::Malformed(
434                    "Merge acknowledgement summary differs from its exact author frontier".into(),
435                ));
436            }
437        }
438        for (device_id, registration) in &self.registrations {
439            if device_id != &registration.reference().device_id
440                || registration.value().store_root.store_root_hash != self.store_root_hash
441            {
442                return Err(StoreProtocolError::DeviceStateMismatch);
443            }
444            registration
445                .reference()
446                .verify_registration(registration.value())?;
447            registration
448                .reference()
449                .object
450                .verify(&registration.value().to_bytes())?;
451            StoreDeviceRegistration::parse_at(
452                &registration.value().to_bytes(),
453                &registration.value().store_root,
454                *device_id,
455            )?;
456        }
457        for (device_id, acknowledgement) in &self.acknowledgements {
458            let registration = self
459                .registrations
460                .get(device_id)
461                .ok_or(StoreProtocolError::DeviceStateMismatch)?;
462            acknowledgement.validate_chain(&registration.value().store_root, registration)?;
463            let (acknowledgement_ref, acknowledgement_value) = acknowledgement
464                .latest()
465                .ok_or(StoreProtocolError::DeviceStateMismatch)?;
466            acknowledgement
467                .activating_commit
468                .verify_commit(&acknowledgement.activating_commit_value)?;
469            if device_id != &acknowledgement_ref.registration.device_id
470                || acknowledgement.activating_commit_value.acknowledgement()
471                    != Some(acknowledgement_ref)
472                || acknowledgement.activating_commit_value.author_registration
473                    != *registration.reference()
474                || self
475                    .causal_cut
476                    .get(&acknowledgement.activating_commit.coord)
477                    != Some(&acknowledgement.activating_commit)
478            {
479                return Err(StoreProtocolError::DeviceStateMismatch);
480            }
481            let predecessor_cut = acknowledgement
482                .activating_commit_value
483                .order
484                .predecessor_cut()?;
485            if acknowledgement_value.store_cut != predecessor_cut
486                || acknowledgement_value.device_state
487                    != acknowledgement.activating_commit_value.device_state
488            {
489                return Err(StoreProtocolError::DeviceStateMismatch);
490            }
491        }
492        for (id, proof) in &self.pending_owner_promotions {
493            proof.validate_shape()?;
494            let request = proof.request()?;
495            let author = self
496                .registrations
497                .get(&request.promoter_registration.device_id)
498                .filter(|author| author.reference() == &request.promoter_registration)
499                .ok_or(StoreProtocolError::OwnerPromotionMismatch)?;
500            if id != &request.promotion_id
501                || request.store_root_hash != self.store_root_hash
502                || !self
503                    .post_state
504                    .frontier()
505                    .covers_commit(&proof.publication.value.commit)
506            {
507                return Err(StoreProtocolError::OwnerPromotionMismatch);
508            }
509            proof
510                .publication
511                .value
512                .verify_for(&proof.commit, author.value())?;
513        }
514        for (activation, closure) in &self.pending_device_joins {
515            closure.accepted_commit(activation)?;
516            let opening = closure.verified_commit(activation)?;
517            if closure.publication.current.store_root_hash != self.store_root_hash
518                || !self.post_state.frontier().covers_commit(activation)
519                || !opening
520                    .device_join_attempt_decisions()
521                    .iter()
522                    .any(|decision| matches!(decision, DeviceJoinAttemptDecisionRef::Attempt(_)))
523                || closure.publication.current.latest_snapshot().is_none()
524            {
525                return Err(StoreProtocolError::DeviceStateMismatch);
526            }
527        }
528        for (reference, proof) in &self.membership_proofs {
529            if reference != &proof.commit
530                || self.causal_cut.get(&proof.commit.coord) != Some(&proof.commit)
531            {
532                return Err(StoreProtocolError::DeviceStateMismatch);
533            }
534            proof.commit.verify_commit(&proof.commit_value)?;
535            let Some(control) = proof.commit_value.control() else {
536                return Err(StoreProtocolError::DeviceStateMismatch);
537            };
538            let transition = &control.transition;
539            if transition.body.entry != proof.entry
540                || proof.entry.coord != proof.entry_value.coord()
541                || !crate::membership::verify_membership_entry(&proof.entry_value)
542            {
543                return Err(StoreProtocolError::DeviceStateMismatch);
544            }
545            proof
546                .entry
547                .object
548                .verify(&serde_json::to_vec(&proof.entry_value)?)?;
549            let head_author = self
550                .registrations
551                .get(&proof.head_value.body.author_registration.device_id)
552                .ok_or(StoreProtocolError::DeviceStateMismatch)?;
553            if !transition.matches_head(&proof.head_value, &proof.head)
554                || !proof.head_value.verify(head_author.value())
555                || !matches!(
556                    &proof.head_value.activation,
557                    crate::membership::MembershipHeadActivation::StoreCommit { commit, .. }
558                        if commit == &proof.commit
559                )
560            {
561                return Err(StoreProtocolError::DeviceStateMismatch);
562            }
563            proof
564                .head
565                .object
566                .verify(&serde_json::to_vec(&proof.head_value)?)?;
567        }
568        Ok(())
569    }
570
571    pub fn validate_snapshot_baseline(&self) -> Result<(), StoreProtocolError> {
572        self.validate_shape()
573    }
574}