1use serde::{Deserialize, Serialize};
4
5use coven_protocol::objects::PreparedExactObject;
6use coven_protocol::prepared_commit::PreparedStoreOperationCommit;
7use coven_protocol::reclaim::{
8 ReclaimAuthorization, ReclaimAuthorizationRef, ReclaimEvidence, ReclaimEvidenceRef,
9 ReclaimReceipt, ReclaimReceiptRef, ReclaimTarget,
10};
11use coven_protocol::remote_object::{
12 CandidateNonactivationProof, RemoteObjectRecord, RemoteObjectRecordError,
13};
14use coven_protocol::store_commit::{ObjectHash, StoreBatchCommitRef, StoreDeviceHeadRef};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case", deny_unknown_fields)]
18pub enum DurableStoreReclaimObject {
19 Authorization {
20 evidence_ref: ReclaimEvidenceRef,
21 evidence: ReclaimEvidence,
22 evidence_prepared: PreparedExactObject,
23 authorization_ref: ReclaimAuthorizationRef,
24 authorization: ReclaimAuthorization,
25 authorization_prepared: PreparedExactObject,
26 },
27 Receipt {
28 receipt_ref: ReclaimReceiptRef,
29 receipt: ReclaimReceipt,
30 receipt_prepared: PreparedExactObject,
31 },
32}
33
34impl DurableStoreReclaimObject {
35 pub fn authorization_ref(&self) -> &ReclaimAuthorizationRef {
36 match self {
37 Self::Authorization {
38 authorization_ref, ..
39 } => authorization_ref,
40 Self::Receipt { receipt_ref, .. } => &receipt_ref.authorization,
41 }
42 }
43
44 pub fn validate(&self) -> Result<(), StoreReclaimJournalError> {
45 match self {
46 Self::Authorization {
47 evidence_ref,
48 evidence,
49 evidence_prepared,
50 authorization_ref,
51 authorization,
52 authorization_prepared,
53 } => {
54 evidence_ref
55 .verify(evidence)
56 .map_err(StoreReclaimJournalError::from)?;
57 authorization_ref
58 .verify_identity(authorization)
59 .map_err(StoreReclaimJournalError::from)?;
60 if evidence_prepared.reference() != &evidence_ref.object
61 || authorization_prepared.reference() != &authorization_ref.object
62 || authorization_ref.evidence != *evidence_ref
63 || authorization.evidence != *evidence_ref
64 || authorization.target != evidence.claim.target()
65 || authorization.store_root_hash != evidence.store_root_hash
66 {
67 return Err(StoreReclaimJournalError::Invalid(
68 "reclaim authorization graph has inconsistent exact identities".to_string(),
69 ));
70 }
71 }
72 Self::Receipt {
73 receipt_ref,
74 receipt,
75 receipt_prepared,
76 } => {
77 receipt_ref
78 .verify_identity(receipt)
79 .map_err(StoreReclaimJournalError::from)?;
80 if receipt_prepared.reference() != &receipt_ref.object {
81 return Err(StoreReclaimJournalError::Invalid(
82 "reclaim receipt differs from its prepared exact object".to_string(),
83 ));
84 }
85 }
86 }
87 Ok(())
88 }
89
90 pub fn commit_names_object(&self, candidate: &PreparedStoreOperationCommit) -> bool {
91 match self {
92 Self::Authorization {
93 authorization_ref, ..
94 } => candidate.commit.reclaim_authorization() == Some(authorization_ref),
95 Self::Receipt { receipt_ref, .. } => {
96 candidate.commit.reclaim_receipt() == Some(receipt_ref)
97 }
98 }
99 }
100
101 pub fn remote_objects(
102 &self,
103 candidate: &PreparedStoreOperationCommit,
104 ) -> Result<Vec<coven_protocol::remote_object::ClosedRemoteObject>, StoreReclaimJournalError>
105 {
106 self.validate()?;
107 if !self.commit_names_object(candidate) {
108 return Err(StoreReclaimJournalError::Invalid(
109 "reclaim candidate does not activate its exact durable object".to_string(),
110 ));
111 }
112 let owner = candidate.reference.clone();
113 let authorities = match self {
114 Self::Authorization {
115 evidence_ref,
116 evidence,
117 evidence_prepared,
118 authorization_ref,
119 authorization,
120 authorization_prepared,
121 } => vec![
122 RemoteObjectRecord::candidate_activated_reclaim_evidence(
123 evidence_ref.clone(),
124 &evidence.to_bytes(),
125 evidence_prepared.stored_bytes(),
126 owner.clone(),
127 )?,
128 RemoteObjectRecord::candidate_activated_reclaim_authorization(
129 authorization_ref.clone(),
130 &authorization.to_bytes(),
131 authorization_prepared.stored_bytes(),
132 owner,
133 )?,
134 ],
135 Self::Receipt {
136 receipt_ref,
137 receipt,
138 receipt_prepared,
139 } => vec![RemoteObjectRecord::candidate_activated_reclaim_receipt(
140 receipt_ref.clone(),
141 &receipt.to_bytes(),
142 receipt_prepared.stored_bytes(),
143 owner,
144 )?],
145 };
146 candidate
147 .retained_authority_remote_objects(authorities)
148 .map_err(StoreReclaimJournalError::Outbound)
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(deny_unknown_fields)]
154pub struct ReclaimCommitActivation {
155 pub commit: StoreBatchCommitRef,
156 pub head: StoreDeviceHeadRef,
157}
158
159impl ReclaimCommitActivation {
160 pub fn new(
161 commit: StoreBatchCommitRef,
162 head: StoreDeviceHeadRef,
163 ) -> Result<Self, StoreReclaimJournalError> {
164 let activation = Self { commit, head };
165 activation.validate()?;
166 Ok(activation)
167 }
168
169 pub fn commit(&self) -> &StoreBatchCommitRef {
170 &self.commit
171 }
172
173 pub fn validate(&self) -> Result<(), StoreReclaimJournalError> {
174 if self.commit.object == self.head.object {
175 return Err(StoreReclaimJournalError::Invalid(
176 "reclaim activation has aliased commit and head identities".to_string(),
177 ));
178 }
179 Ok(())
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "snake_case", deny_unknown_fields)]
185pub enum ReclaimedStorePackage {
186 AbsentVerified {
187 authorization: ReclaimAuthorizationRef,
188 authorization_activation: ReclaimCommitActivation,
189 },
190 Receipted {
191 authorization: ReclaimAuthorizationRef,
192 authorization_activation: ReclaimCommitActivation,
193 receipt: ReclaimReceiptRef,
194 receipt_activation: ReclaimCommitActivation,
195 },
196}
197
198impl ReclaimedStorePackage {
199 pub fn absent_verified(
200 authorization: ReclaimAuthorizationRef,
201 authorization_activation: ReclaimCommitActivation,
202 ) -> Result<Self, StoreReclaimJournalError> {
203 let value = Self::AbsentVerified {
204 authorization,
205 authorization_activation,
206 };
207 value.validate()?;
208 Ok(value)
209 }
210
211 pub fn receipted(
212 authorization: ReclaimAuthorizationRef,
213 authorization_activation: ReclaimCommitActivation,
214 receipt: ReclaimReceiptRef,
215 receipt_activation: ReclaimCommitActivation,
216 ) -> Result<Self, StoreReclaimJournalError> {
217 let value = Self::Receipted {
218 authorization,
219 authorization_activation,
220 receipt,
221 receipt_activation,
222 };
223 value.validate()?;
224 Ok(value)
225 }
226
227 pub fn authorization(&self) -> &ReclaimAuthorizationRef {
228 match self {
229 Self::AbsentVerified { authorization, .. } | Self::Receipted { authorization, .. } => {
230 authorization
231 }
232 }
233 }
234
235 pub fn authorization_activation(&self) -> &ReclaimCommitActivation {
236 match self {
237 Self::AbsentVerified {
238 authorization_activation,
239 ..
240 }
241 | Self::Receipted {
242 authorization_activation,
243 ..
244 } => authorization_activation,
245 }
246 }
247
248 pub fn object_id(&self) -> ObjectHash {
249 coven_protocol::remote_object::remote_object_id(self.authorization().target().object())
250 }
251
252 pub fn validate(&self) -> Result<(), StoreReclaimJournalError> {
253 let authorization = self.authorization();
254 let authorization_activation = self.authorization_activation();
255 validate_reclaim_identity(authorization, authorization_activation)?;
256 let target = authorization.target();
257 let target_activation = authorization.target_activation();
258 if *target.object() == authorization.object
259 || *target.object() == authorization.evidence.object
260 || target.object() == target_activation.object()
261 {
262 return Err(StoreReclaimJournalError::Invalid(
263 "reclaimed package aliases authority or crosses Store histories".to_string(),
264 ));
265 }
266 if let Self::Receipted {
267 receipt,
268 receipt_activation,
269 ..
270 } = self
271 {
272 receipt_activation.validate()?;
273 if &receipt.authorization != authorization
274 || receipt.object == *authorization.target().object()
275 || receipt_activation.commit() == authorization_activation.commit()
276 {
277 return Err(StoreReclaimJournalError::Invalid(
278 "reclaim receipt does not close its exact authorization history".to_string(),
279 ));
280 }
281 }
282 Ok(())
283 }
284}
285
286fn validate_reclaim_identity(
287 authorization: &ReclaimAuthorizationRef,
288 authorization_activation: &ReclaimCommitActivation,
289) -> Result<(), StoreReclaimJournalError> {
290 authorization_activation.validate()?;
291 if authorization.target_activation().object() == &authorization_activation.commit().object {
295 return Err(StoreReclaimJournalError::Invalid(
296 "reclaim authorization does not follow its target in one Store history".to_string(),
297 ));
298 }
299 Ok(())
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303#[serde(rename_all = "snake_case", deny_unknown_fields)]
304pub enum DurableStoreReclaimOperation {
305 AuthorizationCandidate {
306 object: Box<DurableStoreReclaimObject>,
307 candidate: Box<PreparedStoreOperationCommit>,
308 },
309 Authorized {
310 authorization: ReclaimAuthorizationRef,
311 activation: ReclaimCommitActivation,
312 },
313 AbsentVerified {
314 authorization: ReclaimAuthorizationRef,
315 authorization_activation: ReclaimCommitActivation,
316 target: ReclaimTarget,
317 },
318 ReceiptCandidate {
319 authorization: ReclaimAuthorizationRef,
320 authorization_activation: ReclaimCommitActivation,
321 object: Box<DurableStoreReclaimObject>,
322 candidate: Box<PreparedStoreOperationCommit>,
323 },
324 AuthorizationReplacing {
325 object: Box<DurableStoreReclaimObject>,
326 candidate: Box<PreparedStoreOperationCommit>,
327 losing: Box<StoreReclaimCandidateLoss>,
328 },
329 ReceiptReplacing {
330 authorization: ReclaimAuthorizationRef,
331 authorization_activation: ReclaimCommitActivation,
332 object: Box<DurableStoreReclaimObject>,
333 candidate: Box<PreparedStoreOperationCommit>,
334 losing: Box<StoreReclaimCandidateLoss>,
335 },
336 Completed {
337 authorization: ReclaimAuthorizationRef,
338 authorization_activation: ReclaimCommitActivation,
339 receipt: ReclaimReceiptRef,
340 receipt_activation: ReclaimCommitActivation,
341 },
342}
343
344#[derive(Debug, Clone, PartialEq, Eq)]
351pub struct StuckReclaimOperation {
352 pub operation_id: ObjectHash,
353 pub target: ReclaimTarget,
354 pub error: String,
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
358#[serde(deny_unknown_fields)]
359pub struct StoreReclaimCandidateLoss {
360 pub candidate: Box<PreparedStoreOperationCommit>,
361 pub proof: CandidateNonactivationProof,
362}
363
364impl DurableStoreReclaimOperation {
365 pub fn operation_id(&self) -> ObjectHash {
366 self.authorization().authorization_hash
367 }
368
369 pub fn authorization(&self) -> &ReclaimAuthorizationRef {
370 match self {
371 Self::AuthorizationCandidate { object, .. } => object.authorization_ref(),
372 Self::AuthorizationReplacing { object, .. } => object.authorization_ref(),
373 Self::Authorized { authorization, .. }
374 | Self::AbsentVerified { authorization, .. }
375 | Self::ReceiptCandidate { authorization, .. }
376 | Self::ReceiptReplacing { authorization, .. }
377 | Self::Completed { authorization, .. } => authorization,
378 }
379 }
380
381 pub fn candidate(&self) -> Option<&PreparedStoreOperationCommit> {
382 match self {
383 Self::AuthorizationCandidate { candidate, .. }
384 | Self::ReceiptCandidate { candidate, .. }
385 | Self::AuthorizationReplacing { candidate, .. }
386 | Self::ReceiptReplacing { candidate, .. } => Some(candidate),
387 Self::Authorized { .. } | Self::AbsentVerified { .. } | Self::Completed { .. } => None,
388 }
389 }
390
391 pub fn object(&self) -> Option<&DurableStoreReclaimObject> {
392 match self {
393 Self::AuthorizationCandidate { object, .. }
394 | Self::ReceiptCandidate { object, .. }
395 | Self::AuthorizationReplacing { object, .. }
396 | Self::ReceiptReplacing { object, .. } => Some(object),
397 Self::Authorized { .. } | Self::AbsentVerified { .. } | Self::Completed { .. } => None,
398 }
399 }
400
401 pub fn losing_candidate(&self) -> Option<&StoreReclaimCandidateLoss> {
402 match self {
403 Self::AuthorizationReplacing { losing, .. } | Self::ReceiptReplacing { losing, .. } => {
404 Some(losing)
405 }
406 _ => None,
407 }
408 }
409
410 pub fn validate(&self) -> Result<(), StoreReclaimJournalError> {
411 match self {
412 Self::AuthorizationCandidate { object, candidate } => {
413 object.validate()?;
414 candidate
415 .reference
416 .verify_commit(&candidate.commit)
417 .map_err(StoreReclaimJournalError::from)?;
418 if !object.commit_names_object(candidate) {
419 return Err(StoreReclaimJournalError::Invalid(
420 "reclaim journal candidate names another operation".to_string(),
421 ));
422 }
423 }
424 Self::Authorized {
425 authorization,
426 activation,
427 } => validate_reclaim_identity(authorization, activation)?,
428 Self::AbsentVerified {
429 authorization,
430 authorization_activation,
431 target,
432 } => {
433 if target != authorization.target() {
434 return Err(StoreReclaimJournalError::Invalid(
435 "reclaim target differs from its exact authorization".to_string(),
436 ));
437 }
438 ReclaimedStorePackage::absent_verified(
439 authorization.clone(),
440 authorization_activation.clone(),
441 )?;
442 }
443 Self::ReceiptCandidate {
444 authorization,
445 authorization_activation,
446 object,
447 candidate,
448 ..
449 } => {
450 validate_reclaim_identity(authorization, authorization_activation)?;
451 object.validate()?;
452 candidate
453 .reference
454 .verify_commit(&candidate.commit)
455 .map_err(StoreReclaimJournalError::from)?;
456 if object.authorization_ref() != authorization
457 || !matches!(&**object, DurableStoreReclaimObject::Receipt { .. })
458 || !object.commit_names_object(candidate)
459 {
460 return Err(StoreReclaimJournalError::Invalid(
461 "reclaim receipt candidate changes its authorization".to_string(),
462 ));
463 }
464 }
465 Self::AuthorizationReplacing {
466 object,
467 candidate,
468 losing,
469 } => validate_replacement(object, candidate, losing)?,
470 Self::ReceiptReplacing {
471 authorization,
472 authorization_activation,
473 object,
474 candidate,
475 losing,
476 ..
477 } => {
478 validate_reclaim_identity(authorization, authorization_activation)?;
479 validate_replacement(object, candidate, losing)?;
480 if object.authorization_ref() != authorization
481 || !matches!(&**object, DurableStoreReclaimObject::Receipt { .. })
482 {
483 return Err(StoreReclaimJournalError::Invalid(
484 "replacement reclaim receipt changes its authorization".to_string(),
485 ));
486 }
487 }
488 Self::Completed {
489 authorization,
490 authorization_activation,
491 receipt,
492 receipt_activation,
493 } => {
494 ReclaimedStorePackage::receipted(
495 authorization.clone(),
496 authorization_activation.clone(),
497 receipt.clone(),
498 receipt_activation.clone(),
499 )?;
500 }
501 }
502 Ok(())
503 }
504}
505
506fn validate_replacement(
507 object: &DurableStoreReclaimObject,
508 candidate: &PreparedStoreOperationCommit,
509 losing: &StoreReclaimCandidateLoss,
510) -> Result<(), StoreReclaimJournalError> {
511 object.validate()?;
512 candidate
513 .reference
514 .verify_commit(&candidate.commit)
515 .map_err(StoreReclaimJournalError::from)?;
516 losing
517 .candidate
518 .reference
519 .verify_commit(&losing.candidate.commit)
520 .map_err(StoreReclaimJournalError::from)?;
521 coven_protocol::remote_object::CandidateNonactivation::validate_durable_shape(
522 &losing.candidate.reference,
523 &losing.candidate.commit,
524 losing.proof.clone(),
525 )?;
526 if candidate.reference == losing.candidate.reference
527 || !object.commit_names_object(candidate)
528 || !object.commit_names_object(&losing.candidate)
529 {
530 return Err(StoreReclaimJournalError::Invalid(
531 "replacement reclaim candidate changes its signed object".to_string(),
532 ));
533 }
534 Ok(())
535}
536
537#[derive(Debug, thiserror::Error)]
538pub enum StoreReclaimJournalError {
539 #[error("invalid durable Store reclaim state: {0}")]
540 Invalid(String),
541 #[error(transparent)]
542 RemoteObject(#[from] RemoteObjectRecordError),
543 #[error(transparent)]
544 Outbound(#[from] coven_protocol::prepared_commit::PreparedCommitError),
545 #[error(transparent)]
546 Storage(#[from] coven_protocol::objects::StorageError),
547 #[error(transparent)]
548 Protocol(#[from] coven_protocol::store_commit::StoreProtocolError),
549}