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