Skip to main content

ic_backup/policy/ic_observation/capture_settlement/
mod.rs

1//! Pure admission of independently qualified original capture settlement.
2
3use super::{
4    IcObservationAssociationError, IcObservationReplyView, IcObservationResponseView,
5    validate_response,
6};
7use crate::{
8    model::{
9        artifacts::ArtifactChecksumRecord,
10        attempt_journal::{AttemptJournalRecord, ObservationOutcomeRecord},
11        ic_observation::{
12            IcCaptureAttribution, IcCaptureSettlement, IcObservationRequest, IcObservationResponse,
13        },
14        ic_request::IcManagementMethodRecord as Method,
15        ic_snapshot_reply::{IcSnapshotInfo, IcSnapshotReply},
16    },
17    policy::{
18        inventory_settlement::{
19            InventorySettlementIdentity, InventorySettlementMismatch, new_candidate_index,
20        },
21        snapshot_inventory_delta::{SnapshotInventoryDeltaError, compare_inventories},
22    },
23};
24use thiserror::Error;
25
26/// Matched passive capture claim and exact inventories; never an authenticated receipt.
27#[derive(Debug)]
28pub struct IcCaptureSettlementView<'a> {
29    observation: IcObservationResponseView<'a>,
30    baseline: &'a IcSnapshotReply<'a>,
31    settlement: &'a IcCaptureSettlement,
32    captured_snapshot: Option<usize>,
33    outcome: ObservationOutcomeRecord,
34}
35impl<'a> IcCaptureSettlementView<'a> {
36    /// Read the independent claim; integration qualification precedes explicit recording.
37    #[must_use]
38    pub const fn outcome(&self) -> ObservationOutcomeRecord {
39        self.outcome
40    }
41    /// Read the original independently retained full baseline.
42    #[must_use]
43    pub const fn baseline(&self) -> &'a IcSnapshotReply<'a> {
44        self.baseline
45    }
46    /// Read the exact current reserved list association.
47    #[must_use]
48    pub const fn observation(&self) -> &IcObservationResponseView<'a> {
49        &self.observation
50    }
51    /// Read the complete retained integration claims.
52    #[must_use]
53    pub const fn settlement(&self) -> &'a IcCaptureSettlement {
54        self.settlement
55    }
56    /// Read only the explicitly attributed new descriptor, without singleton inference.
57    #[must_use]
58    pub fn captured_snapshot(&self) -> Option<&IcSnapshotInfo> {
59        match self.observation.reply() {
60            IcObservationReplyView::Inventory(reply) => self
61                .captured_snapshot
62                .map(|index| &reply.snapshots()[index]),
63            IcObservationReplyView::Status(_) => None,
64        }
65    }
66}
67
68/// Bind independently qualified capture settlement to exact original/current inventories.
69///
70/// Reuses current reservations, actual claims, bounded decoding and the canonical
71/// closed-baseline comparison. Every original ID and its metadata must remain unchanged.
72/// Applied names an explicitly attributed new ID; zero/one/many candidates infer no
73/// outcome. Negative evidence excludes transient capture/deletion. Unresolved needs
74/// an actually settled authenticated list; lost replies remain pending.
75///
76/// Integrations own original baseline chronology, actual authentication, freshness,
77/// read permission, attribution and custody. Matching digests establish none of those.
78/// This performs no IO, calls, serialization, receipt transition, retries, refunds,
79/// consistency/transfer qualification or terminal/fence/reference release admission.
80/// # Errors
81/// Rejects lifecycle/status lanes, changed reservations/claims/wire, baseline loss or
82/// drift, settlement identity drift and an Applied ID outside the exact new candidates.
83pub fn validate_capture_settlement<'a>(
84    request: &IcObservationRequest<'a>,
85    journal: &AttemptJournalRecord,
86    baseline: &'a IcSnapshotReply<'a>,
87    response: &'a IcObservationResponse,
88    challenge: &ArtifactChecksumRecord,
89    settlement: &'a IcCaptureSettlement,
90) -> Result<IcCaptureSettlementView<'a>, IcCaptureSettlementError> {
91    if request.mutation().method() != Method::TakeCanisterSnapshot
92        || request.payload().method() != Method::ListCanisterSnapshots
93    {
94        return Err(IcCaptureSettlementError::UnsupportedMethod);
95    }
96    let observation = validate_response(request, journal, response)?;
97    let IcObservationReplyView::Inventory(inventory) = observation.reply() else {
98        return Err(IcCaptureSettlementError::UnsupportedMethod);
99    };
100    let candidates = compare_inventories(request.mutation().target(), baseline, inventory)?;
101    let authority = request.authority().digest();
102    let baseline_digest = baseline.digest();
103    let inventory_digest = inventory.digest();
104    InventorySettlementIdentity {
105        authority: &settlement.authority,
106        mutation_attempt: settlement.mutation_attempt,
107        observation_attempt: settlement.observation_attempt,
108        challenge: &settlement.challenge,
109        baseline: &settlement.baseline,
110        inventory: &settlement.inventory,
111        observation_evidence: &settlement.observation_evidence,
112    }
113    .compare(&InventorySettlementIdentity {
114        authority: &authority,
115        mutation_attempt: request.mutation_attempt(),
116        observation_attempt: request.observation_attempt(),
117        challenge,
118        baseline: &baseline_digest,
119        inventory: &inventory_digest,
120        observation_evidence: &response.input().evidence,
121    })?;
122    let (outcome, captured_snapshot) = match &settlement.attribution {
123        IcCaptureAttribution::Applied { snapshot_id, .. } => {
124            let index = new_candidate_index(inventory, &candidates, snapshot_id)
125                .ok_or(IcCaptureSettlementError::NotNewCandidate)?;
126            (ObservationOutcomeRecord::Applied, Some(index))
127        }
128        IcCaptureAttribution::NotApplied { .. } => (ObservationOutcomeRecord::NotApplied, None),
129        IcCaptureAttribution::Unresolved { .. } => (ObservationOutcomeRecord::Uncertain, None),
130    };
131    Ok(IcCaptureSettlementView {
132        observation,
133        baseline,
134        settlement,
135        captured_snapshot,
136        outcome,
137    })
138}
139
140/// Typed local claim denial; original allowances, obligations and references stay retained.
141#[derive(Debug, Error)]
142pub enum IcCaptureSettlementError {
143    /// Only original non-replacing capture and a reserved list observation are supported.
144    #[error("capture settlement requires original capture and list observation")]
145    UnsupportedMethod,
146    /// Existing association, current reservation or finite wire admission rejected.
147    #[error(transparent)]
148    Observation(#[from] IcObservationAssociationError),
149    /// Existing closed full-baseline comparison rejected.
150    #[error(transparent)]
151    Baseline(#[from] SnapshotInventoryDeltaError),
152    /// Complete original operation authority differs.
153    #[error("capture settlement original authority differs")]
154    AuthorityMismatch,
155    /// Original mutation or observation attempt differs.
156    #[error("capture settlement attempt identities differ")]
157    AttemptMismatch,
158    /// Caller-owned current qualification challenge differs.
159    #[error("capture settlement challenge differs")]
160    ChallengeMismatch,
161    /// Exact original baseline request/raw evidence differs.
162    #[error("capture settlement original baseline differs")]
163    BaselineMismatch,
164    /// Exact current inventory request/raw evidence differs.
165    #[error("capture settlement current inventory differs")]
166    InventoryMismatch,
167    /// Opaque observation evidence differs even if raw reply bytes match.
168    #[error("capture settlement observation evidence differs")]
169    ObservationEvidenceMismatch,
170    /// The attributed raw ID is not a bounded new candidate in the current inventory.
171    #[error("capture settlement ID is not a new candidate")]
172    NotNewCandidate,
173}
174
175impl From<InventorySettlementMismatch> for IcCaptureSettlementError {
176    fn from(value: InventorySettlementMismatch) -> Self {
177        match value {
178            InventorySettlementMismatch::Authority => Self::AuthorityMismatch,
179            InventorySettlementMismatch::Attempt => Self::AttemptMismatch,
180            InventorySettlementMismatch::Challenge => Self::ChallengeMismatch,
181            InventorySettlementMismatch::Baseline => Self::BaselineMismatch,
182            InventorySettlementMismatch::Inventory => Self::InventoryMismatch,
183            InventorySettlementMismatch::ObservationEvidence => Self::ObservationEvidenceMismatch,
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests;