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::{RemoteObjectRecord, RemoteObjectRecordError};
12use coven_protocol::store_commit::{ObjectHash, StoreBatchCommitRef};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case", deny_unknown_fields)]
16pub enum DurableStoreReclaimObject {
17    Authorization {
18        evidence_ref: ReclaimEvidenceRef,
19        evidence: ReclaimEvidence,
20        evidence_prepared: PreparedExactObject,
21        authorization_ref: ReclaimAuthorizationRef,
22        authorization: ReclaimAuthorization,
23        authorization_prepared: PreparedExactObject,
24    },
25    Receipt {
26        receipt_ref: ReclaimReceiptRef,
27        receipt: ReclaimReceipt,
28        receipt_prepared: PreparedExactObject,
29    },
30}
31
32impl DurableStoreReclaimObject {
33    pub fn authorization_ref(&self) -> &ReclaimAuthorizationRef {
34        match self {
35            Self::Authorization {
36                authorization_ref, ..
37            } => authorization_ref,
38            Self::Receipt { receipt_ref, .. } => &receipt_ref.authorization,
39        }
40    }
41
42    pub fn validate(&self) -> Result<(), StoreReclaimJournalError> {
43        match self {
44            Self::Authorization {
45                evidence_ref,
46                evidence,
47                evidence_prepared,
48                authorization_ref,
49                authorization,
50                authorization_prepared,
51            } => {
52                evidence_ref
53                    .verify(evidence)
54                    .map_err(StoreReclaimJournalError::from)?;
55                authorization_ref
56                    .verify_identity(authorization)
57                    .map_err(StoreReclaimJournalError::from)?;
58                if evidence_prepared.reference() != &evidence_ref.object
59                    || authorization_prepared.reference() != &authorization_ref.object
60                    || authorization_ref.evidence != *evidence_ref
61                    || authorization.evidence != *evidence_ref
62                    || authorization.target != evidence.claim.target()
63                    || authorization.store_root_hash != evidence.store_root_hash
64                {
65                    return Err(StoreReclaimJournalError::Invalid(
66                        "reclaim authorization graph has inconsistent exact identities".to_string(),
67                    ));
68                }
69            }
70            Self::Receipt {
71                receipt_ref,
72                receipt,
73                receipt_prepared,
74            } => {
75                receipt_ref
76                    .verify_identity(receipt)
77                    .map_err(StoreReclaimJournalError::from)?;
78                if receipt_prepared.reference() != &receipt_ref.object {
79                    return Err(StoreReclaimJournalError::Invalid(
80                        "reclaim receipt differs from its prepared exact object".to_string(),
81                    ));
82                }
83            }
84        }
85        Ok(())
86    }
87
88    pub fn commit_names_object(&self, candidate: &PreparedStoreOperationCommit) -> bool {
89        match self {
90            Self::Authorization {
91                authorization_ref, ..
92            } => candidate.commit.reclaim_authorization() == Some(authorization_ref),
93            Self::Receipt { receipt_ref, .. } => {
94                candidate.commit.reclaim_receipt() == Some(receipt_ref)
95            }
96        }
97    }
98
99    pub fn remote_objects(
100        &self,
101        candidate: &PreparedStoreOperationCommit,
102    ) -> Result<Vec<coven_protocol::remote_object::ClosedRemoteObject>, StoreReclaimJournalError>
103    {
104        self.validate()?;
105        if !self.commit_names_object(candidate) {
106            return Err(StoreReclaimJournalError::Invalid(
107                "reclaim candidate does not activate its exact durable object".to_string(),
108            ));
109        }
110        let owner = candidate.reference.clone();
111        let authorities = match self {
112            Self::Authorization {
113                evidence_ref,
114                evidence,
115                evidence_prepared,
116                authorization_ref,
117                authorization,
118                authorization_prepared,
119            } => vec![
120                RemoteObjectRecord::candidate_activated_reclaim_evidence(
121                    evidence_ref.clone(),
122                    &evidence.to_bytes(),
123                    evidence_prepared.stored_bytes(),
124                    owner.clone(),
125                )?,
126                RemoteObjectRecord::candidate_activated_reclaim_authorization(
127                    authorization_ref.clone(),
128                    &authorization.to_bytes(),
129                    authorization_prepared.stored_bytes(),
130                    owner,
131                )?,
132            ],
133            Self::Receipt {
134                receipt_ref,
135                receipt,
136                receipt_prepared,
137            } => vec![RemoteObjectRecord::candidate_activated_reclaim_receipt(
138                receipt_ref.clone(),
139                &receipt.to_bytes(),
140                receipt_prepared.stored_bytes(),
141                owner,
142            )?],
143        };
144        candidate
145            .retained_authority_remote_objects(authorities)
146            .map_err(StoreReclaimJournalError::Outbound)
147    }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "snake_case", deny_unknown_fields)]
152pub enum ReclaimedStorePackage {
153    AbsentVerified {
154        authorization: ReclaimAuthorizationRef,
155        authorization_activation: StoreBatchCommitRef,
156    },
157    Receipted {
158        authorization: ReclaimAuthorizationRef,
159        authorization_activation: StoreBatchCommitRef,
160        receipt: ReclaimReceiptRef,
161        receipt_activation: StoreBatchCommitRef,
162    },
163}
164
165impl ReclaimedStorePackage {
166    pub fn absent_verified(
167        authorization: ReclaimAuthorizationRef,
168        authorization_activation: StoreBatchCommitRef,
169    ) -> Result<Self, StoreReclaimJournalError> {
170        let value = Self::AbsentVerified {
171            authorization,
172            authorization_activation,
173        };
174        value.validate()?;
175        Ok(value)
176    }
177
178    pub fn receipted(
179        authorization: ReclaimAuthorizationRef,
180        authorization_activation: StoreBatchCommitRef,
181        receipt: ReclaimReceiptRef,
182        receipt_activation: StoreBatchCommitRef,
183    ) -> Result<Self, StoreReclaimJournalError> {
184        let value = Self::Receipted {
185            authorization,
186            authorization_activation,
187            receipt,
188            receipt_activation,
189        };
190        value.validate()?;
191        Ok(value)
192    }
193
194    pub fn authorization(&self) -> &ReclaimAuthorizationRef {
195        match self {
196            Self::AbsentVerified { authorization, .. } | Self::Receipted { authorization, .. } => {
197                authorization
198            }
199        }
200    }
201
202    pub fn authorization_activation(&self) -> &StoreBatchCommitRef {
203        match self {
204            Self::AbsentVerified {
205                authorization_activation,
206                ..
207            }
208            | Self::Receipted {
209                authorization_activation,
210                ..
211            } => authorization_activation,
212        }
213    }
214
215    pub fn object_id(&self) -> ObjectHash {
216        coven_protocol::remote_object::remote_object_id(self.authorization().target().object())
217    }
218
219    pub fn validate(&self) -> Result<(), StoreReclaimJournalError> {
220        let authorization = self.authorization();
221        let authorization_activation = self.authorization_activation();
222        validate_reclaim_identity(authorization, authorization_activation)?;
223        let target = authorization.target();
224        let target_activation = authorization.target_activation();
225        if *target.object() == authorization.object
226            || *target.object() == authorization.evidence.object
227            || target_activation.names_authority_object(target.object())
228        {
229            return Err(StoreReclaimJournalError::Invalid(
230                "reclaimed package aliases authority or crosses Store histories".to_string(),
231            ));
232        }
233        if let Self::Receipted {
234            receipt,
235            receipt_activation,
236            ..
237        } = self
238        {
239            if &receipt.authorization != authorization
240                || receipt.object == *authorization.target().object()
241                || receipt_activation == authorization_activation
242            {
243                return Err(StoreReclaimJournalError::Invalid(
244                    "reclaim receipt does not close its exact authorization history".to_string(),
245                ));
246            }
247        }
248        Ok(())
249    }
250}
251
252fn validate_reclaim_identity(
253    authorization: &ReclaimAuthorizationRef,
254    authorization_activation: &StoreBatchCommitRef,
255) -> Result<(), StoreReclaimJournalError> {
256    // The commit carrying the authorization must follow whatever activated the
257    // target, never be it — an Owner cannot authorize a reclaim in the same signed
258    // statement that published the object.
259    if authorization
260        .target_activation()
261        .names_authority_object(&authorization_activation.object)
262    {
263        return Err(StoreReclaimJournalError::Invalid(
264            "reclaim authorization does not follow its target in one Store history".to_string(),
265        ));
266    }
267    Ok(())
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
271#[serde(rename_all = "snake_case", deny_unknown_fields)]
272pub enum DurableStoreReclaimOperation {
273    AuthorizationCandidate {
274        object: Box<DurableStoreReclaimObject>,
275        candidate: Box<PreparedStoreOperationCommit>,
276    },
277    Authorized {
278        authorization: ReclaimAuthorizationRef,
279        activation: StoreBatchCommitRef,
280    },
281    AbsentVerified {
282        authorization: ReclaimAuthorizationRef,
283        authorization_activation: StoreBatchCommitRef,
284        target: ReclaimTarget,
285    },
286    ReceiptCandidate {
287        authorization: ReclaimAuthorizationRef,
288        authorization_activation: StoreBatchCommitRef,
289        object: Box<DurableStoreReclaimObject>,
290        candidate: Box<PreparedStoreOperationCommit>,
291    },
292    Completed {
293        authorization: ReclaimAuthorizationRef,
294        authorization_activation: StoreBatchCommitRef,
295        receipt: ReclaimReceiptRef,
296        receipt_activation: StoreBatchCommitRef,
297    },
298}
299
300/// A journalled reclaim operation whose last run failed with an error that
301/// running it again cannot change.
302///
303/// Every later cycle skips it, so it spends no provider requests and holds
304/// nothing else up; it runs again only when the host asks. The target and the
305/// error are what the host shows the person who has to decide.
306#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct StuckReclaimOperation {
308    pub operation_id: ObjectHash,
309    pub target: ReclaimTarget,
310    pub error: String,
311}
312
313impl DurableStoreReclaimOperation {
314    pub fn operation_id(&self) -> ObjectHash {
315        self.authorization().authorization_hash
316    }
317
318    pub fn authorization(&self) -> &ReclaimAuthorizationRef {
319        match self {
320            Self::AuthorizationCandidate { object, .. } => object.authorization_ref(),
321            Self::Authorized { authorization, .. }
322            | Self::AbsentVerified { authorization, .. }
323            | Self::ReceiptCandidate { authorization, .. }
324            | Self::Completed { authorization, .. } => authorization,
325        }
326    }
327
328    pub fn candidate(&self) -> Option<&PreparedStoreOperationCommit> {
329        match self {
330            Self::AuthorizationCandidate { candidate, .. }
331            | Self::ReceiptCandidate { candidate, .. } => Some(candidate),
332            Self::Authorized { .. } | Self::AbsentVerified { .. } | Self::Completed { .. } => None,
333        }
334    }
335
336    pub fn object(&self) -> Option<&DurableStoreReclaimObject> {
337        match self {
338            Self::AuthorizationCandidate { object, .. } | Self::ReceiptCandidate { object, .. } => {
339                Some(object)
340            }
341            Self::Authorized { .. } | Self::AbsentVerified { .. } | Self::Completed { .. } => None,
342        }
343    }
344
345    pub fn validate(&self) -> Result<(), StoreReclaimJournalError> {
346        match self {
347            Self::AuthorizationCandidate { object, candidate } => {
348                object.validate()?;
349                candidate
350                    .reference
351                    .verify_commit(&candidate.commit)
352                    .map_err(StoreReclaimJournalError::from)?;
353                if !object.commit_names_object(candidate) {
354                    return Err(StoreReclaimJournalError::Invalid(
355                        "reclaim journal candidate names another operation".to_string(),
356                    ));
357                }
358            }
359            Self::Authorized {
360                authorization,
361                activation,
362            } => validate_reclaim_identity(authorization, activation)?,
363            Self::AbsentVerified {
364                authorization,
365                authorization_activation,
366                target,
367            } => {
368                if target != authorization.target() {
369                    return Err(StoreReclaimJournalError::Invalid(
370                        "reclaim target differs from its exact authorization".to_string(),
371                    ));
372                }
373                ReclaimedStorePackage::absent_verified(
374                    authorization.clone(),
375                    authorization_activation.clone(),
376                )?;
377            }
378            Self::ReceiptCandidate {
379                authorization,
380                authorization_activation,
381                object,
382                candidate,
383                ..
384            } => {
385                validate_reclaim_identity(authorization, authorization_activation)?;
386                object.validate()?;
387                candidate
388                    .reference
389                    .verify_commit(&candidate.commit)
390                    .map_err(StoreReclaimJournalError::from)?;
391                if object.authorization_ref() != authorization
392                    || !matches!(&**object, DurableStoreReclaimObject::Receipt { .. })
393                    || !object.commit_names_object(candidate)
394                {
395                    return Err(StoreReclaimJournalError::Invalid(
396                        "reclaim receipt candidate changes its authorization".to_string(),
397                    ));
398                }
399            }
400            Self::Completed {
401                authorization,
402                authorization_activation,
403                receipt,
404                receipt_activation,
405            } => {
406                ReclaimedStorePackage::receipted(
407                    authorization.clone(),
408                    authorization_activation.clone(),
409                    receipt.clone(),
410                    receipt_activation.clone(),
411                )?;
412            }
413        }
414        Ok(())
415    }
416}
417
418#[derive(Debug, thiserror::Error)]
419pub enum StoreReclaimJournalError {
420    #[error("invalid durable Store reclaim state: {0}")]
421    Invalid(String),
422    #[error(transparent)]
423    RemoteObject(#[from] RemoteObjectRecordError),
424    #[error(transparent)]
425    Outbound(#[from] coven_protocol::prepared_commit::PreparedCommitError),
426    #[error(transparent)]
427    Storage(#[from] coven_protocol::objects::StorageError),
428    #[error(transparent)]
429    Protocol(#[from] coven_protocol::store_commit::StoreProtocolError),
430}