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        require_capture(plan, requirement)?;
92        Self::new(
93            plan,
94            acquisition_operation,
95            FenceObligationScopeRecord::Capture {
96                requirement: requirement.digest(),
97                identity: fence.identity.clone(),
98                membership_revision: fence.membership_revision.clone(),
99            },
100        )
101    }
102    /// Bind restoration to the exact original source, fence and revisions before acquisition.
103    /// # Errors
104    /// Rejects changed plans, a non-fenced safety lane, absent operation or zero mutation allowance.
105    pub fn for_restore(
106        plan: &OperationPlanRecord,
107        source: &OperationPlanRecord,
108        requirement: &RestoreSafetyRequirementRecord,
109        acquisition_operation: u64,
110    ) -> Result<Self, FenceObligationError> {
111        let fence = require_restore(plan, source, requirement)?;
112        Self::new(
113            plan,
114            acquisition_operation,
115            FenceObligationScopeRecord::Restore {
116                requirement: requirement.digest(),
117                fence: fence.clone(),
118            },
119        )
120    }
121    fn new(
122        plan: &OperationPlanRecord,
123        acquisition_operation: u64,
124        scope: FenceObligationScopeRecord,
125    ) -> Result<Self, FenceObligationError> {
126        let record = Self {
127            version: 1,
128            plan_intent: plan.digest(),
129            acquisition_operation,
130            scope,
131        };
132        record.validate_plan(plan)?;
133        Ok(record)
134    }
135    /// Read the full original plan digest, including exact selection, requests and allowances.
136    #[must_use]
137    pub const fn plan_intent(&self) -> &ArtifactChecksumRecord {
138        &self.plan_intent
139    }
140    /// Read the declared explicit acquisition operation; not a dispatch permit.
141    #[must_use]
142    pub const fn acquisition_operation(&self) -> u64 {
143        self.acquisition_operation
144    }
145    /// Read immutable requirement and fence bindings; not an Active/released projection.
146    #[must_use]
147    pub const fn scope(&self) -> &FenceObligationScopeRecord {
148        &self.scope
149    }
150    /// Match the full original plan and its finite acquisition allowance.
151    /// # Errors
152    /// Rejects changed plan identity, missing operation or zero mutation allowance.
153    pub fn validate_plan(&self, plan: &OperationPlanRecord) -> Result<(), FenceObligationError> {
154        if self.plan_intent != plan.digest() {
155            return Err(FenceObligationError::PlanMismatch);
156        }
157        require_acquisition(plan, self.acquisition_operation)
158    }
159    /// Match every original capture binding without weakening scope on recovery.
160    /// # Errors
161    /// Rejects another original requirement, fence, revision, plan or purpose.
162    pub fn validate_capture(
163        &self,
164        plan: &OperationPlanRecord,
165        requirement: &ConsistencyRequirementRecord,
166        fence: &ApplicationFenceBinding,
167    ) -> Result<(), FenceObligationError> {
168        require_capture(plan, requirement)?;
169        require_acquisition(plan, self.acquisition_operation)?;
170        let scope_matches = matches!(
171            &self.scope,
172            FenceObligationScopeRecord::Capture { requirement: original, identity, membership_revision }
173                if original == &requirement.digest()
174                    && identity == &fence.identity
175                    && membership_revision == &fence.membership_revision
176        );
177        self.validate_binding(plan, scope_matches)
178    }
179    /// Match original restore/source/fence bindings without treating their hashes as custody.
180    /// # Errors
181    /// Rejects changed requirements, source, revisions, plans or purpose.
182    pub fn validate_restore(
183        &self,
184        plan: &OperationPlanRecord,
185        source: &OperationPlanRecord,
186        requirement: &RestoreSafetyRequirementRecord,
187    ) -> Result<(), FenceObligationError> {
188        let fence = require_restore(plan, source, requirement)?;
189        require_acquisition(plan, self.acquisition_operation)?;
190        let scope_matches = matches!(
191            &self.scope,
192            FenceObligationScopeRecord::Restore { requirement: original, fence: original_fence }
193                if original == &requirement.digest() && original_fence == fence
194        );
195        self.validate_binding(plan, scope_matches)
196    }
197    // Match original declarations after purpose and allowance admission, retaining
198    // BindingMismatch for drift rather than changing the public rejection order.
199    fn validate_binding(
200        &self,
201        plan: &OperationPlanRecord,
202        scope_matches: bool,
203    ) -> Result<(), FenceObligationError> {
204        if self.plan_intent != plan.digest() || !scope_matches {
205            return Err(FenceObligationError::BindingMismatch);
206        }
207        Ok(())
208    }
209    /// Hash NUL-terminated v1 domain, original intent, big-endian u64 operation,
210    /// purpose tag (capture=0, restore=1), requirement and original fence revisions.
211    ///
212    /// All hashes are 64 lowercase ASCII bytes. Restore additionally includes its
213    /// external-obligations revision. No mutable status, receipts or release flag exists.
214    #[must_use]
215    pub fn digest(&self) -> ArtifactChecksumRecord {
216        let mut bytes = EvidenceDigest::new(b"ic-backup/fence-obligation/v1\0");
217        bytes.append(self.plan_intent.hash().as_bytes());
218        bytes.append(&self.acquisition_operation.to_be_bytes());
219        let (tag, hashes) = match &self.scope {
220            FenceObligationScopeRecord::Capture {
221                requirement,
222                identity,
223                membership_revision,
224            } => (0, vec![requirement, identity, membership_revision]),
225            FenceObligationScopeRecord::Restore { requirement, fence } => (
226                1,
227                vec![
228                    requirement,
229                    &fence.identity,
230                    &fence.membership_revision,
231                    &fence.external_obligations_revision,
232                ],
233            ),
234        };
235        bytes.byte(tag);
236        for hash in hashes {
237            bytes.append(hash.hash().as_bytes());
238        }
239        bytes.finish()
240    }
241}
242
243fn require_capture(
244    plan: &OperationPlanRecord,
245    requirement: &ConsistencyRequirementRecord,
246) -> Result<(), FenceObligationError> {
247    requirement.validate_plan(plan)?;
248    if requirement.guarantee() != ConsistencyGuaranteeRecord::ApplicationCoordinated {
249        return Err(FenceObligationError::FenceNotRequired);
250    }
251    Ok(())
252}
253
254fn require_restore<'a>(
255    plan: &OperationPlanRecord,
256    source: &OperationPlanRecord,
257    requirement: &'a RestoreSafetyRequirementRecord,
258) -> Result<&'a RestoreFenceBindingRecord, FenceObligationError> {
259    requirement.validate_plans(plan, source)?;
260    if requirement.safety() != RestoreSafetyLaneRecord::ApplicationFenced {
261        return Err(FenceObligationError::FenceNotRequired);
262    }
263    requirement
264        .expected_fence()
265        .ok_or(FenceObligationError::FenceNotRequired)
266}
267
268fn require_acquisition(
269    plan: &OperationPlanRecord,
270    sequence: u64,
271) -> Result<(), FenceObligationError> {
272    // The admitted plan already owns canonical bindings. Reading its allowance
273    // does not require constructing a new authority and rehashing the full plan.
274    if plan.operation(sequence)?.budget().mutations() == 0 {
275        return Err(FenceObligationError::NoAcquisitionAllowance);
276    }
277    Ok(())
278}
279/// Typed original obligation rejection; never releases a fence or resets attempts.
280#[derive(Debug, Error)]
281pub enum FenceObligationError {
282    /// Only the maintained v1 schema is admitted.
283    #[error("unsupported fence obligation version {0}")]
284    UnsupportedVersion(u16),
285    /// Full original plan differs.
286    #[error("fence obligation original plan mismatch")]
287    PlanMismatch,
288    /// Exact original purpose, requirement or fence revisions differ.
289    #[error("fence obligation original binding mismatch")]
290    BindingMismatch,
291    /// The original guarantee/safety lane requires no fence.
292    #[error("original requirement does not require an application fence")]
293    FenceNotRequired,
294    /// The selected original acquisition operation has no mutation allowance.
295    #[error("original fence acquisition operation has no mutation allowance")]
296    NoAcquisitionAllowance,
297    /// Original operation is absent or cannot derive its authority.
298    #[error(transparent)]
299    Plan(#[from] OperationPlanError),
300    /// Capture declaration mismatch.
301    #[error(transparent)]
302    Consistency(#[from] ConsistencyRequirementError),
303    /// Restore/source declaration mismatch.
304    #[error(transparent)]
305    Restore(#[from] RestoreSafetyRequirementError),
306}
307
308#[cfg(test)]
309mod tests;