coven_protocol/store_commit/
reclaim_state.rs1use super::*;
2use crate::reclaim::{AudienceBlobBindingPackage, ReclaimAuthorizationRef, ReclaimTarget};
3
4#[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#[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#[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 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}