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;