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.checksum(&self.plan_intent);
218        bytes.u64_be(self.acquisition_operation);
219        match &self.scope {
220            FenceObligationScopeRecord::Capture {
221                requirement,
222                identity,
223                membership_revision,
224            } => {
225                bytes.byte(0);
226                bytes.checksum(requirement);
227                bytes.checksum(identity);
228                bytes.checksum(membership_revision);
229            }
230            FenceObligationScopeRecord::Restore { requirement, fence } => {
231                bytes.byte(1);
232                bytes.checksum(requirement);
233                bytes.checksum(&fence.identity);
234                bytes.checksum(&fence.membership_revision);
235                bytes.checksum(&fence.external_obligations_revision);
236            }
237        }
238        bytes.finish()
239    }
240}
241
242fn require_capture(
243    plan: &OperationPlanRecord,
244    requirement: &ConsistencyRequirementRecord,
245) -> Result<(), FenceObligationError> {
246    requirement.validate_plan(plan)?;
247    if requirement.guarantee() != ConsistencyGuaranteeRecord::ApplicationCoordinated {
248        return Err(FenceObligationError::FenceNotRequired);
249    }
250    Ok(())
251}
252
253fn require_restore<'a>(
254    plan: &OperationPlanRecord,
255    source: &OperationPlanRecord,
256    requirement: &'a RestoreSafetyRequirementRecord,
257) -> Result<&'a RestoreFenceBindingRecord, FenceObligationError> {
258    requirement.validate_plans(plan, source)?;
259    if requirement.safety() != RestoreSafetyLaneRecord::ApplicationFenced {
260        return Err(FenceObligationError::FenceNotRequired);
261    }
262    requirement
263        .expected_fence()
264        .ok_or(FenceObligationError::FenceNotRequired)
265}
266
267fn require_acquisition(
268    plan: &OperationPlanRecord,
269    sequence: u64,
270) -> Result<(), FenceObligationError> {
271    // The admitted plan already owns canonical bindings. Reading its allowance
272    // does not require constructing a new authority and rehashing the full plan.
273    if plan.operation(sequence)?.budget().mutations() == 0 {
274        return Err(FenceObligationError::NoAcquisitionAllowance);
275    }
276    Ok(())
277}
278/// Typed original obligation rejection; never releases a fence or resets attempts.
279#[derive(Debug, Error)]
280pub enum FenceObligationError {
281    /// Only the maintained v1 schema is admitted.
282    #[error("unsupported fence obligation version {0}")]
283    UnsupportedVersion(u16),
284    /// Full original plan differs.
285    #[error("fence obligation original plan mismatch")]
286    PlanMismatch,
287    /// Exact original purpose, requirement or fence revisions differ.
288    #[error("fence obligation original binding mismatch")]
289    BindingMismatch,
290    /// The original guarantee/safety lane requires no fence.
291    #[error("original requirement does not require an application fence")]
292    FenceNotRequired,
293    /// The selected original acquisition operation has no mutation allowance.
294    #[error("original fence acquisition operation has no mutation allowance")]
295    NoAcquisitionAllowance,
296    /// Original operation is absent or cannot derive its authority.
297    #[error(transparent)]
298    Plan(#[from] OperationPlanError),
299    /// Capture declaration mismatch.
300    #[error(transparent)]
301    Consistency(#[from] ConsistencyRequirementError),
302    /// Restore/source declaration mismatch.
303    #[error(transparent)]
304    Restore(#[from] RestoreSafetyRequirementError),
305}
306
307#[cfg(test)]
308mod tests;