Skip to main content

coven_database/store/store_session/reclaim/
journal.rs

1//! Durable publication and execution state for exact Store reclaim operations.
2
3use 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    // The commit carrying the authorization must follow whatever activated the
292    // target, never be it — an Owner cannot authorize a reclaim in the same signed
293    // statement that published the object.
294    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/// A journalled reclaim operation whose last run failed with an error that
345/// running it again cannot change.
346///
347/// Every later cycle skips it, so it spends no provider requests and holds
348/// nothing else up; it runs again only when the host asks. The target and the
349/// error are what the host shows the person who has to decide.
350#[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}