Skip to main content

ic_backup/model/fence_obligation/
mod.rs

1//! Immutable original application fence obligations; spending belongs to attempt journals.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    consistency::{
6        ApplicationFenceBinding, ConsistencyGuaranteeRecord, ConsistencyRequirementError,
7        ConsistencyRequirementRecord,
8    },
9    operation_plan::{OperationPlanError, OperationPlanRecord},
10    restore_safety::{
11        RestoreFenceBindingRecord, RestoreSafetyLaneRecord, RestoreSafetyRequirementError,
12        RestoreSafetyRequirementRecord,
13    },
14};
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17
18/// Maximum raw input and canonical output bytes for one original obligation.
19pub const MAX_FENCE_OBLIGATION_BYTES: u64 = 1024;
20
21/// Exact original requirement and fence revisions, without current lifecycle state.
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
23#[serde(tag = "purpose", rename_all = "snake_case", deny_unknown_fields)]
24pub enum FenceObligationScopeRecord {
25    /// Whole-selection application-coordinated capture.
26    Capture {
27        /// Exact original consistency requirement digest.
28        requirement: ArtifactChecksumRecord,
29        /// Original integration-chosen fence identity, retained before dispatch.
30        identity: ArtifactChecksumRecord,
31        /// Original membership authority revision.
32        membership_revision: ArtifactChecksumRecord,
33    },
34    /// Same-release restoration under an outside-snapshot application fence.
35    Restore {
36        /// Exact original restore safety requirement digest, including source identity.
37        requirement: ArtifactChecksumRecord,
38        /// Original fence and membership/external-obligation authority revisions.
39        fence: RestoreFenceBindingRecord,
40    },
41}
42
43/// Strict immutable v1 obligation for one whole original selected unit.
44///
45/// An acquisition operation is an explicit opaque application request in the
46/// original plan. Its physical target identifies routing only; it does not narrow
47/// fence coverage to that target. Integrations qualify request semantics, original
48/// identity custody and scope. This declaration proves no acquisition, Active
49/// state, absence of external obligations, release or spending authority.
50#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51#[serde(try_from = "ObligationFields")]
52pub struct FenceObligationRecord {
53    version: u16,
54    plan_intent: ArtifactChecksumRecord,
55    acquisition_operation: u64,
56    scope: FenceObligationScopeRecord,
57}
58#[derive(Deserialize)]
59#[serde(deny_unknown_fields)]
60struct ObligationFields {
61    version: u16,
62    plan_intent: ArtifactChecksumRecord,
63    acquisition_operation: u64,
64    scope: FenceObligationScopeRecord,
65}
66impl TryFrom<ObligationFields> for FenceObligationRecord {
67    type Error = FenceObligationError;
68    fn try_from(fields: ObligationFields) -> Result<Self, Self::Error> {
69        if fields.version != 1 {
70            return Err(FenceObligationError::UnsupportedVersion(fields.version));
71        }
72        Ok(Self {
73            version: 1,
74            plan_intent: fields.plan_intent,
75            acquisition_operation: fields.acquisition_operation,
76            scope: fields.scope,
77        })
78    }
79}
80impl FenceObligationRecord {
81    /// Bind a coordinated capture obligation before attempting its explicit acquisition operation.
82    /// # Errors
83    /// Rejects another plan, a weaker guarantee, absent operation or zero mutation allowance.
84    pub fn for_capture(
85        plan: &OperationPlanRecord,
86        requirement: &ConsistencyRequirementRecord,
87        acquisition_operation: u64,
88        fence: &ApplicationFenceBinding,
89    ) -> Result<Self, FenceObligationError> {
90        requirement.validate_plan(plan)?;
91        if requirement.guarantee() != ConsistencyGuaranteeRecord::ApplicationCoordinated {
92            return Err(FenceObligationError::FenceNotRequired);
93        }
94        Self::new(
95            plan,
96            acquisition_operation,
97            FenceObligationScopeRecord::Capture {
98                requirement: requirement.digest(),
99                identity: fence.identity.clone(),
100                membership_revision: fence.membership_revision.clone(),
101            },
102        )
103    }
104    /// Bind restoration to the exact original source, fence and revisions before acquisition.
105    /// # Errors
106    /// Rejects changed plans, a non-fenced safety lane, absent operation or zero mutation allowance.
107    pub fn for_restore(
108        plan: &OperationPlanRecord,
109        source: &OperationPlanRecord,
110        requirement: &RestoreSafetyRequirementRecord,
111        acquisition_operation: u64,
112    ) -> Result<Self, FenceObligationError> {
113        requirement.validate_plans(plan, source)?;
114        if requirement.safety() != RestoreSafetyLaneRecord::ApplicationFenced {
115            return Err(FenceObligationError::FenceNotRequired);
116        }
117        let fence = requirement
118            .expected_fence()
119            .ok_or(FenceObligationError::FenceNotRequired)?;
120        Self::new(
121            plan,
122            acquisition_operation,
123            FenceObligationScopeRecord::Restore {
124                requirement: requirement.digest(),
125                fence: fence.clone(),
126            },
127        )
128    }
129    fn new(
130        plan: &OperationPlanRecord,
131        acquisition_operation: u64,
132        scope: FenceObligationScopeRecord,
133    ) -> Result<Self, FenceObligationError> {
134        let record = Self {
135            version: 1,
136            plan_intent: plan.digest(),
137            acquisition_operation,
138            scope,
139        };
140        record.validate_plan(plan)?;
141        Ok(record)
142    }
143    /// Read the full original plan digest, including exact selection, requests and allowances.
144    #[must_use]
145    pub const fn plan_intent(&self) -> &ArtifactChecksumRecord {
146        &self.plan_intent
147    }
148    /// Read the declared explicit acquisition operation; not a dispatch permit.
149    #[must_use]
150    pub const fn acquisition_operation(&self) -> u64 {
151        self.acquisition_operation
152    }
153    /// Read immutable requirement and fence bindings; not an Active/released projection.
154    #[must_use]
155    pub const fn scope(&self) -> &FenceObligationScopeRecord {
156        &self.scope
157    }
158    /// Match the full original plan and its finite acquisition allowance.
159    /// # Errors
160    /// Rejects changed plan identity, missing operation or zero mutation allowance.
161    pub fn validate_plan(&self, plan: &OperationPlanRecord) -> Result<(), FenceObligationError> {
162        if self.plan_intent != plan.digest() {
163            return Err(FenceObligationError::PlanMismatch);
164        }
165        if plan
166            .attempt_authority(self.acquisition_operation)?
167            .budget()
168            .mutations()
169            == 0
170        {
171            return Err(FenceObligationError::NoAcquisitionAllowance);
172        }
173        Ok(())
174    }
175    /// Match every original capture binding without weakening scope on recovery.
176    /// # Errors
177    /// Rejects another original requirement, fence, revision, plan or purpose.
178    pub fn validate_capture(
179        &self,
180        plan: &OperationPlanRecord,
181        requirement: &ConsistencyRequirementRecord,
182        fence: &ApplicationFenceBinding,
183    ) -> Result<(), FenceObligationError> {
184        let original = Self::for_capture(plan, requirement, self.acquisition_operation, fence)?;
185        self.validate_original(&original)
186    }
187    /// Match original restore/source/fence bindings without treating their hashes as custody.
188    /// # Errors
189    /// Rejects changed requirements, source, revisions, plans or purpose.
190    pub fn validate_restore(
191        &self,
192        plan: &OperationPlanRecord,
193        source: &OperationPlanRecord,
194        requirement: &RestoreSafetyRequirementRecord,
195    ) -> Result<(), FenceObligationError> {
196        let original = Self::for_restore(plan, source, requirement, self.acquisition_operation)?;
197        self.validate_original(&original)
198    }
199    fn validate_original(&self, original: &Self) -> Result<(), FenceObligationError> {
200        if self != original {
201            return Err(FenceObligationError::BindingMismatch);
202        }
203        Ok(())
204    }
205    /// Hash NUL-terminated v1 domain, original intent, big-endian u64 operation,
206    /// purpose tag (capture=0, restore=1), requirement and original fence revisions.
207    ///
208    /// All hashes are 64 lowercase ASCII bytes. Restore additionally includes its
209    /// external-obligations revision. No mutable status, receipts or release flag exists.
210    #[must_use]
211    pub fn digest(&self) -> ArtifactChecksumRecord {
212        let mut bytes = b"ic-backup/fence-obligation/v1\0".to_vec();
213        bytes.extend_from_slice(self.plan_intent.hash().as_bytes());
214        bytes.extend_from_slice(&self.acquisition_operation.to_be_bytes());
215        let (tag, hashes) = match &self.scope {
216            FenceObligationScopeRecord::Capture {
217                requirement,
218                identity,
219                membership_revision,
220            } => (0, vec![requirement, identity, membership_revision]),
221            FenceObligationScopeRecord::Restore { requirement, fence } => (
222                1,
223                vec![
224                    requirement,
225                    &fence.identity,
226                    &fence.membership_revision,
227                    &fence.external_obligations_revision,
228                ],
229            ),
230        };
231        bytes.push(tag);
232        for hash in hashes {
233            bytes.extend_from_slice(hash.hash().as_bytes());
234        }
235        ArtifactChecksumRecord::from_bytes(&bytes)
236    }
237}
238/// Typed original obligation rejection; never releases a fence or resets attempts.
239#[derive(Debug, Error)]
240pub enum FenceObligationError {
241    /// Only the maintained v1 schema is admitted.
242    #[error("unsupported fence obligation version {0}")]
243    UnsupportedVersion(u16),
244    /// Full original plan differs.
245    #[error("fence obligation original plan mismatch")]
246    PlanMismatch,
247    /// Exact original purpose, requirement or fence revisions differ.
248    #[error("fence obligation original binding mismatch")]
249    BindingMismatch,
250    /// The original guarantee/safety lane requires no fence.
251    #[error("original requirement does not require an application fence")]
252    FenceNotRequired,
253    /// The selected original acquisition operation has no mutation allowance.
254    #[error("original fence acquisition operation has no mutation allowance")]
255    NoAcquisitionAllowance,
256    /// Original operation is absent or cannot derive its authority.
257    #[error(transparent)]
258    Plan(#[from] OperationPlanError),
259    /// Capture declaration mismatch.
260    #[error(transparent)]
261    Consistency(#[from] ConsistencyRequirementError),
262    /// Restore/source declaration mismatch.
263    #[error(transparent)]
264    Restore(#[from] RestoreSafetyRequirementError),
265}
266
267#[cfg(test)]
268mod tests;