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