Skip to main content

ic_backup/model/consistency/
mod.rs

1//! Original declared consistency and ephemeral current capture/fence evidence.
2
3mod requirement;
4pub use requirement::{
5    ConsistencyGuaranteeRecord, ConsistencyRequirementError, ConsistencyRequirementRecord,
6    MAX_CONSISTENCY_REQUIREMENT_BYTES,
7};
8
9use crate::model::{
10    artifacts::ArtifactChecksumRecord,
11    attempt_journal::OperationBindingRecord,
12    evidence_digest::EvidenceDigest,
13    inventory::{InventoryRecord, MAX_INVENTORY_TARGETS},
14    operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
15};
16use thiserror::Error;
17
18/// Maximum descriptive remote observations per invocation; no paid-call allowance.
19pub const MAX_CONSISTENCY_REMOTE_OBSERVATIONS: u32 = 1024;
20/// Explicit capture boundary; before/after equality alone establishes no continuity.
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum ConsistencyBoundary {
23    /// Check current evidence before capture under independently qualified effect admission.
24    BeforeCapture,
25    /// Check current evidence after capture; not a completion receipt.
26    AfterCapture,
27}
28/// Exact integration-retained application fence and original membership revision.
29///
30/// Integrations recover this binding from durable obligation evidence; constructing
31/// it proves neither retention nor current active custody and grants no release authority.
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct ApplicationFenceBinding {
34    /// Exact retained fence identity, never a release token.
35    pub identity: ArtifactChecksumRecord,
36    /// Original membership revision bound when the retained fence was established.
37    pub membership_revision: ArtifactChecksumRecord,
38}
39/// Passive request parameters; no default capture or fence authority.
40#[derive(Clone, Debug)]
41pub struct ConsistencyRequestInput {
42    /// Exact original operation sequence; request payload remains bound through full intent.
43    pub operation_sequence: u64,
44    /// Fresh integration-owned challenge; digest binding alone proves no freshness.
45    pub challenge: ArtifactChecksumRecord,
46    /// Explicit capture boundary.
47    pub boundary: ConsistencyBoundary,
48    /// Retained exact fence/revision binding required only for coordinated capture.
49    pub expected_fence: Option<ApplicationFenceBinding>,
50    /// Descriptive call ceiling, independent of any original or preflight spending allowance.
51    pub max_remote_observations: u32,
52}
53/// Ephemeral current request under the exact original consistency declaration and plan.
54#[derive(Clone, Debug)]
55pub struct ConsistencyRequest<'a> {
56    plan: &'a OperationPlanRecord,
57    requirement: &'a ConsistencyRequirementRecord,
58    binding: OperationBindingRecord,
59    input: ConsistencyRequestInput,
60}
61impl<'a> ConsistencyRequest<'a> {
62    /// Bind original requirement/operation to an explicit challenge/boundary/fence identity.
63    ///
64    /// The integration recovers the expected fence from its durable obligation owner.
65    /// This constructor cannot acquire/recover/release a fence or inspect that evidence.
66    /// # Errors
67    /// Rejects plan mismatch, unknown operation, inappropriate/missing fence or excess ceiling.
68    pub fn new(
69        plan: &'a OperationPlanRecord,
70        requirement: &'a ConsistencyRequirementRecord,
71        input: ConsistencyRequestInput,
72    ) -> Result<Self, ConsistencyRequestError> {
73        requirement.validate_plan(plan)?;
74        if input.max_remote_observations > MAX_CONSISTENCY_REMOTE_OBSERVATIONS {
75            return Err(ConsistencyRequestError::ObservationLimitTooLarge);
76        }
77        match (requirement.guarantee(), input.expected_fence.as_ref()) {
78            (ConsistencyGuaranteeRecord::ApplicationCoordinated, None) => {
79                return Err(ConsistencyRequestError::FenceRequired);
80            }
81            (ConsistencyGuaranteeRecord::PerCanister, Some(_)) => {
82                return Err(ConsistencyRequestError::UnexpectedFence);
83            }
84            _ => {}
85        }
86        let binding = plan.operation_binding(input.operation_sequence)?;
87        Ok(Self {
88            plan,
89            requirement,
90            binding,
91            input,
92        })
93    }
94    /// Read exact original mutation identity; no codec, dispatch or current permission admission.
95    #[must_use]
96    pub const fn binding(&self) -> &OperationBindingRecord {
97        &self.binding
98    }
99    /// Read full original inventory, including unselected parents/metadata.
100    #[must_use]
101    pub const fn inventory(&self) -> &InventoryRecord {
102        self.plan.inventory()
103    }
104    /// Read exact original selected principals in canonical order, not dispatch order.
105    #[must_use]
106    pub fn selected_targets(&self) -> &[String] {
107        self.plan.selected_targets()
108    }
109    /// Read immutable original requested guarantee.
110    #[must_use]
111    pub const fn requirement(&self) -> &ConsistencyRequirementRecord {
112        self.requirement
113    }
114    /// Read caller-owned challenge; matching digests alone are not freshness proof.
115    #[must_use]
116    pub const fn challenge(&self) -> &ArtifactChecksumRecord {
117        &self.input.challenge
118    }
119    /// Read explicit capture boundary.
120    #[must_use]
121    pub const fn boundary(&self) -> ConsistencyBoundary {
122        self.input.boundary
123    }
124    /// Read retained exact expected fence/revision binding; not proof of current active custody.
125    #[must_use]
126    pub const fn expected_fence(&self) -> Option<&ApplicationFenceBinding> {
127        self.input.expected_fence.as_ref()
128    }
129    /// Read descriptive call ceiling; grants no spending/retry admission.
130    #[must_use]
131    pub const fn max_remote_observations(&self) -> u32 {
132        self.input.max_remote_observations
133    }
134    /// Hash original requirement, operation, challenge, boundary, expected fence and ceiling.
135    ///
136    /// Domain is NUL-terminated ASCII v1, then 64 ASCII requirement-digest bytes,
137    /// u64 BE sequence, 64 ASCII challenge bytes, boundary byte (before=0/after=1),
138    /// fence presence byte (0/1), optional 64 ASCII identity and 64 ASCII original
139    /// membership-revision bytes, then u32 BE ceiling.
140    #[must_use]
141    pub fn digest(&self) -> ArtifactChecksumRecord {
142        let mut framing = EvidenceDigest::new(b"ic-backup/consistency-request/v1\0");
143        framing.checksum(&self.requirement.digest());
144        framing.u64_be(self.binding.operation_sequence());
145        framing.checksum(self.challenge());
146        framing.byte(match self.boundary() {
147            ConsistencyBoundary::BeforeCapture => 0,
148            ConsistencyBoundary::AfterCapture => 1,
149        });
150        match self.expected_fence() {
151            None => framing.byte(0),
152            Some(fence) => {
153                framing.byte(1);
154                framing.checksum(&fence.identity);
155                framing.checksum(&fence.membership_revision);
156            }
157        }
158        framing.u32_be(self.max_remote_observations());
159        framing.finish()
160    }
161}
162/// Actual observed lifecycle at the capture boundary; not a management reply codec.
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
164pub enum CaptureState {
165    /// Target is currently running; deny capture consistency admission.
166    Running,
167    /// Target has not completed stopping; deny capture consistency admission.
168    Stopping,
169    /// Target is observed stopped; integration must also qualify drained-work evidence.
170    Stopped,
171}
172/// Passive exact target state and opaque stopped/drained evidence qualified by its owner.
173#[derive(Clone, Debug)]
174pub struct TargetCaptureEvidence {
175    /// Actually observed physical target, canonicalized at observation admission.
176    pub target: String,
177    /// Actually observed lifecycle; process success cannot substitute.
178    pub state: CaptureState,
179    /// Required integration-qualified stopped/drained-work evidence; digest alone is no proof.
180    pub stopped_and_drained: ArtifactChecksumRecord,
181}
182impl super::observed_targets::ObservedTargetRow for TargetCaptureEvidence {
183    fn target(&self) -> &str {
184        &self.target
185    }
186
187    fn target_mut(&mut self) -> &mut String {
188        &mut self.target
189    }
190}
191/// Actually observed application fence state; not a serialized accepted/proven flag.
192#[derive(Clone, Copy, Debug, Eq, PartialEq)]
193pub enum ApplicationFenceState {
194    /// Exact retained fence is qualified currently active with continuous custody.
195    Active,
196    /// Exact retained fence is known inactive; deny coordinated consistency.
197    Inactive,
198}
199/// Passive whole-selection fence evidence qualified by the application integration.
200///
201/// All evidence applies to the exact current request/context/full inventory/selection.
202/// The integration proves continuous retained custody across the capture window and
203/// interruption. Matching identities/revisions alone cannot prove that property.
204#[derive(Clone, Debug)]
205pub struct ApplicationFenceEvidence {
206    /// Actually observed active/inactive state; unknown custody must fail at the provider.
207    pub state: ApplicationFenceState,
208    /// Exact active retained fence identity; never a release token.
209    pub identity: ArtifactChecksumRecord,
210    /// Qualified current membership authority revision bound to this active fence.
211    pub membership_revision: ArtifactChecksumRecord,
212    /// Evidence application writes are fenced for the whole selected unit.
213    pub writes: ArtifactChecksumRecord,
214    /// Evidence membership changes are fenced for the whole selected unit.
215    pub membership: ArtifactChecksumRecord,
216    /// Evidence relevant application timers are fenced.
217    pub timers: ArtifactChecksumRecord,
218    /// Evidence relevant external work is fenced; not restore/payment settlement.
219    pub external_work: ArtifactChecksumRecord,
220    /// Evidence application work has drained under retained fence custody.
221    pub drained_work: ArtifactChecksumRecord,
222}
223/// Current declared evidence lane; neither variant is self-authenticating.
224#[derive(Clone, Debug)]
225pub enum ConsistencyEvidence {
226    /// Only each exact stopped target is qualified; no atomic application claim.
227    PerCanister,
228    /// Application integration qualifies this exact currently active whole-selection fence.
229    ApplicationCoordinated(Box<ApplicationFenceEvidence>),
230}
231/// Passive provider data; accepted flags, expiry/defaults and JSON authority admission are absent.
232#[derive(Clone, Debug)]
233pub struct ConsistencyObservationInput {
234    /// Exact current request digest.
235    pub request: ArtifactChecksumRecord,
236    /// Actually observed canonical network/caller/release.
237    pub context: PlanContextRecord,
238    /// Complete actually observed current inventory, including unselected parent metadata.
239    pub inventory: InventoryRecord,
240    /// Exact actual selected target states/evidence; admitted canonically by the model.
241    pub targets: Vec<TargetCaptureEvidence>,
242    /// Actually observed optional membership revision; coordinated evidence requires a match.
243    pub membership_revision: Option<ArtifactChecksumRecord>,
244    /// Actual current evidence lane; policy requires the original declared guarantee.
245    pub consistency: ConsistencyEvidence,
246    /// Required opaque integration-qualified observation evidence.
247    pub evidence: ArtifactChecksumRecord,
248    /// Actual remote observations, each requiring separate prior approved accounting.
249    pub remote_observations: u32,
250}
251/// Immutable canonical current evidence; not persisted proof or a fence-release capability.
252#[derive(Clone, Debug)]
253pub struct ConsistencyObservation {
254    input: ConsistencyObservationInput,
255}
256impl ConsistencyObservation {
257    /// Normalize/sort exact targets, require a nonempty bounded unique inventory-backed set.
258    /// # Errors
259    /// Rejects target count, malformed identities, canonical duplicates or unknown targets.
260    pub fn new(
261        mut input: ConsistencyObservationInput,
262    ) -> Result<Self, ConsistencyObservationError> {
263        input.targets = super::observed_targets::canonicalize(
264            input.targets,
265            &input.inventory,
266            || ConsistencyObservationError::InvalidTargetCount,
267            || ConsistencyObservationError::InvalidPrincipal,
268            || ConsistencyObservationError::TargetAbsentFromInventory,
269            || ConsistencyObservationError::DuplicateTarget,
270        )?;
271        Ok(Self { input })
272    }
273    /// Read current request identity.
274    #[must_use]
275    pub const fn request(&self) -> &ArtifactChecksumRecord {
276        &self.input.request
277    }
278    /// Read actually observed canonical context.
279    #[must_use]
280    pub const fn context(&self) -> &PlanContextRecord {
281        &self.input.context
282    }
283    /// Read complete actual current inventory.
284    #[must_use]
285    pub const fn inventory(&self) -> &InventoryRecord {
286        &self.input.inventory
287    }
288    /// Read exact canonical actual target state/evidence rows.
289    #[must_use]
290    pub fn targets(&self) -> &[TargetCaptureEvidence] {
291        &self.input.targets
292    }
293    /// Read actual membership revision; equality alone grants no continuity.
294    #[must_use]
295    pub const fn membership_revision(&self) -> Option<&ArtifactChecksumRecord> {
296        self.input.membership_revision.as_ref()
297    }
298    /// Read current evidence lane.
299    #[must_use]
300    pub const fn consistency(&self) -> &ConsistencyEvidence {
301        &self.input.consistency
302    }
303    /// Read opaque qualified observation evidence.
304    #[must_use]
305    pub const fn evidence(&self) -> &ArtifactChecksumRecord {
306        &self.input.evidence
307    }
308    /// Read actual call reporting without spending/replenishment.
309    #[must_use]
310    pub const fn remote_observations(&self) -> u32 {
311        self.input.remote_observations
312    }
313}
314/// Typed current canonical evidence admission failure.
315#[derive(Debug, Eq, Error, PartialEq)]
316pub enum ConsistencyObservationError {
317    /// Exact actual targets must fit the inventory bound and be nonempty.
318    #[error("consistency targets must contain 1..={MAX_INVENTORY_TARGETS} entries")]
319    InvalidTargetCount,
320    /// Target principal is invalid or exceeds its own bound.
321    #[error("invalid consistency target principal")]
322    InvalidPrincipal,
323    /// Equivalent target identities cannot appear twice.
324    #[error("duplicate consistency target")]
325    DuplicateTarget,
326    /// Actual target is not in the actually observed inventory.
327    #[error("consistency target absent from actual inventory")]
328    TargetAbsentFromInventory,
329}
330/// Typed original requirement/request denial; no failure acquires/releases a fence.
331#[derive(Debug, Error)]
332pub enum ConsistencyRequestError {
333    /// Original requirement does not match the original plan.
334    #[error(transparent)]
335    Requirement(#[from] ConsistencyRequirementError),
336    /// Original plan rejects exact operation derivation.
337    #[error(transparent)]
338    Plan(#[from] OperationPlanError),
339    /// Coordinated capture must name the retained exact fence obligation.
340    #[error("coordinated consistency requires retained fence identity")]
341    FenceRequired,
342    /// Per-canister guarantee cannot silently acquire/upgrade an application fence.
343    #[error("per-canister consistency does not admit expected fence identity")]
344    UnexpectedFence,
345    /// Descriptive call ceiling exceeds its maintained bound.
346    #[error("consistency observation ceiling exceeds {MAX_CONSISTENCY_REMOTE_OBSERVATIONS}")]
347    ObservationLimitTooLarge,
348}
349
350#[cfg(test)]
351mod tests;