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