Skip to main content

coven_protocol/store_commit/
reclaim_state.rs

1use super::*;
2use crate::reclaim::{AudienceBlobBindingPackage, ReclaimAuthorizationRef, ReclaimTarget};
3
4/// Exact package provenance retained while the remote package still exists.
5/// Snapshot authentication carries this fact after its commit is retired.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(deny_unknown_fields)]
8pub struct RetainedPackageActivation {
9    pub package: AudienceBlobBindingPackage,
10    pub activation: StoreBatchCommitRef,
11    pub commit: StoreBatchCommit,
12}
13
14impl RetainedPackageActivation {
15    pub fn matches_package(
16        &self,
17        package: &AudienceBlobBindingPackage,
18        activation: &StoreBatchCommitRef,
19    ) -> bool {
20        &self.package == package && &self.activation == activation
21    }
22
23    fn validate(&self) -> Result<(), StoreProtocolError> {
24        self.activation.verify_commit(&self.commit)?;
25        let named = match &self.package {
26            AudienceBlobBindingPackage::Store(package) => {
27                self.commit.store_package() == Some(package)
28            }
29            AudienceBlobBindingPackage::Circle(package) => {
30                self.commit.circle_packages().contains(package)
31            }
32        };
33        if !named {
34            return Err(StoreProtocolError::Malformed(
35                "retained package differs from its exact activating commit".into(),
36            ));
37        }
38        Ok(())
39    }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct RetainedReclaimAuthorization {
45    pub authorization: ReclaimAuthorizationRef,
46    pub activation: StoreBatchCommitRef,
47}
48
49/// Exact artifacts of a superseded accepted snapshot awaiting physical retirement.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct RetainedStoreSnapshotOwnership {
53    pub accepted: AcceptedStoreSnapshotRef,
54    pub image: SnapshotImageRef,
55    pub rollup: MembershipRollupRef,
56}
57
58impl RetainedStoreSnapshotOwnership {
59    pub fn objects(&self) -> [&ExactObjectRef; 4] {
60        [
61            &self.image.object,
62            &self.rollup.object,
63            &self.accepted.snapshot.object,
64            &self.accepted.publication.object,
65        ]
66    }
67}
68
69/// Reclamation's live objects and unfinished accepted authorizations.
70/// Receipts release their exact facts; ordinary covered commits are not retained.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(deny_unknown_fields)]
73pub struct RetainedReclaimState {
74    #[serde(with = "ordered_map_entries")]
75    pub snapshots: BTreeMap<ObjectHash, RetainedStoreSnapshotOwnership>,
76    #[serde(with = "ordered_map_entries")]
77    pub publications: BTreeMap<ObjectHash, StorePublicationRef>,
78    #[serde(with = "ordered_map_entries")]
79    pub packages: BTreeMap<ObjectHash, RetainedPackageActivation>,
80    #[serde(with = "ordered_map_entries")]
81    pub authorizations: BTreeMap<ObjectHash, RetainedReclaimAuthorization>,
82}
83
84impl RetainedReclaimState {
85    pub fn genesis() -> Self {
86        Self {
87            snapshots: BTreeMap::new(),
88            publications: BTreeMap::new(),
89            packages: BTreeMap::new(),
90            authorizations: BTreeMap::new(),
91        }
92    }
93
94    pub fn validate(&self) -> Result<(), StoreProtocolError> {
95        for (id, snapshot) in &self.snapshots {
96            snapshot.accepted.publication.validate_slot()?;
97            crate::objects::ProtocolObjectContext::signed_plaintext(
98                snapshot.accepted.publication.store_root_hash,
99                crate::objects::ProtocolObjectDomain::StoreSnapshotMeta,
100            )
101            .validate_reference(
102                &snapshot.accepted.snapshot.object,
103                &semantic_prefix_from_exact_object(&snapshot.accepted.snapshot.object, ".json")?,
104            )?;
105            snapshot
106                .accepted
107                .snapshot
108                .validate_artifact_slots(&snapshot.image, &snapshot.rollup)?;
109            if *id != snapshot.accepted.snapshot.snapshot_hash
110                || snapshot.rollup.object == snapshot.accepted.snapshot.object
111            {
112                return Err(StoreProtocolError::Malformed(
113                    "retained snapshot rollup has inconsistent exact ownership".to_string(),
114                ));
115            }
116        }
117        for (id, publication) in &self.publications {
118            if *id != crate::remote_object::remote_object_id(&publication.object) {
119                return Err(StoreProtocolError::Malformed(
120                    "retired publication has inconsistent exact identity".into(),
121                ));
122            }
123            publication.validate_slot()?;
124        }
125        for (id, package) in &self.packages {
126            package.validate()?;
127            if *id != crate::remote_object::remote_object_id(package.package.object())
128                || package.package.object() == &package.activation.object
129            {
130                return Err(StoreProtocolError::Malformed(
131                    "retained reclaim package has inconsistent exact provenance".to_string(),
132                ));
133            }
134        }
135        for (id, authorization) in &self.authorizations {
136            if *id != authorization.authorization.authorization_hash
137                || authorization.authorization.object == authorization.activation.object
138            {
139                return Err(StoreProtocolError::Malformed(
140                    "retained reclaim authorization has inconsistent exact activation".to_string(),
141                ));
142            }
143        }
144        Ok(())
145    }
146
147    pub fn validate_before(
148        &self,
149        successor: &StorePublicationRef,
150    ) -> Result<(), StoreProtocolError> {
151        self.validate()?;
152        for publication in self.publications.values().chain(
153            self.snapshots
154                .values()
155                .map(|snapshot| &snapshot.accepted.publication),
156        ) {
157            if publication.store_root_hash != successor.store_root_hash
158                || publication.position >= successor.position
159            {
160                return Err(StoreProtocolError::Malformed(
161                    "retired artifact is not before its accepted successor".into(),
162                ));
163            }
164        }
165        Ok(())
166    }
167
168    pub fn include_previous_snapshot(
169        &mut self,
170        accepted: &AcceptedStoreSnapshotRef,
171        metadata: &SnapshotMeta,
172    ) -> Result<(), StoreProtocolError> {
173        if metadata.snapshot_hash() != accepted.snapshot.snapshot_hash
174            || metadata.publication_predecessor.next_position()? != accepted.publication.position
175        {
176            return Err(StoreProtocolError::Malformed(
177                "retained snapshot ownership differs from its accepted boundary".to_string(),
178            ));
179        }
180        self.snapshots = metadata.history_summary.reclaim.snapshots.clone();
181        self.publications = metadata.history_summary.reclaim.publications.clone();
182        self.snapshots.insert(
183            accepted.snapshot.snapshot_hash,
184            RetainedStoreSnapshotOwnership {
185                accepted: accepted.clone(),
186                image: metadata.image.clone(),
187                rollup: metadata.membership_rollup.clone(),
188            },
189        );
190        Ok(())
191    }
192
193    /// Fold the accepted interval as a set of additions and corresponding receipts.
194    /// Traversal order does not change which exact objects the interval retires.
195    pub fn extend<'a>(
196        &mut self,
197        commits: impl IntoIterator<Item = (&'a StoreBatchCommitRef, &'a StoreBatchCommit)>,
198    ) -> Result<(), StoreProtocolError> {
199        let commits = commits.into_iter().collect::<Vec<_>>();
200        for (reference, commit) in &commits {
201            reference.verify_commit(commit)?;
202            let packages = commit
203                .store_package()
204                .cloned()
205                .map(AudienceBlobBindingPackage::Store)
206                .into_iter()
207                .chain(
208                    commit
209                        .circle_packages()
210                        .iter()
211                        .cloned()
212                        .map(AudienceBlobBindingPackage::Circle),
213                );
214            for package in packages {
215                let id = crate::remote_object::remote_object_id(package.object());
216                let value = RetainedPackageActivation {
217                    package,
218                    activation: (*reference).clone(),
219                    commit: (*commit).clone(),
220                };
221                if self
222                    .packages
223                    .get(&id)
224                    .is_some_and(|existing| existing != &value)
225                {
226                    return Err(StoreProtocolError::Malformed(
227                        "accepted packages disagree on exact activation".to_string(),
228                    ));
229                }
230                self.packages.insert(id, value);
231            }
232            if let Some(authorization) = commit.reclaim_authorization() {
233                let value = RetainedReclaimAuthorization {
234                    authorization: authorization.clone(),
235                    activation: (*reference).clone(),
236                };
237                if self
238                    .authorizations
239                    .get(&authorization.authorization_hash)
240                    .is_some_and(|existing| existing != &value)
241                {
242                    return Err(StoreProtocolError::Malformed(
243                        "reclaim authorization has conflicting accepted activations".to_string(),
244                    ));
245                }
246                self.authorizations
247                    .insert(authorization.authorization_hash, value);
248            }
249        }
250        self.retire_receipts(
251            commits
252                .into_iter()
253                .filter_map(|(_, commit)| commit.reclaim_receipt()),
254        )
255    }
256
257    pub fn retire_receipts<'a>(
258        &mut self,
259        receipts: impl IntoIterator<Item = &'a crate::reclaim::ReclaimReceiptRef>,
260    ) -> Result<(), StoreProtocolError> {
261        for receipt in receipts {
262            let target = receipt.authorization.target();
263            if matches!(
264                target,
265                ReclaimTarget::StorePackage(_) | ReclaimTarget::CirclePackage(_)
266            ) {
267                let id = crate::remote_object::remote_object_id(target.object());
268                if let Some(package) = self.packages.get(&id) {
269                    let matches = match target {
270                        ReclaimTarget::StorePackage(target) => {
271                            package.package
272                                == AudienceBlobBindingPackage::Store(target.package.clone())
273                                && package.activation == target.activation
274                        }
275                        ReclaimTarget::CirclePackage(target) => {
276                            package.package
277                                == AudienceBlobBindingPackage::Circle(target.package.clone())
278                                && package.activation == target.activation
279                        }
280                        _ => unreachable!("matched package target"),
281                    };
282                    if !matches {
283                        return Err(StoreProtocolError::Malformed(
284                            "reclaim receipt names another package activation".to_string(),
285                        ));
286                    }
287                }
288                self.packages.remove(&id);
289            }
290            if let Some(authorization) = self
291                .authorizations
292                .get(&receipt.authorization.authorization_hash)
293            {
294                if authorization.authorization != receipt.authorization {
295                    return Err(StoreProtocolError::Malformed(
296                        "reclaim receipt names another exact authorization".to_string(),
297                    ));
298                }
299            }
300            self.authorizations
301                .remove(&receipt.authorization.authorization_hash);
302        }
303        self.validate()
304    }
305}