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