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        match self.observation.reply() {
62            IcObservationReplyView::Inventory(reply) => self
63                .captured_snapshot
64                .map(|index| &reply.snapshots()[index]),
65            IcObservationReplyView::Status(_) => None,
66        }
67    }
68}
69
70/// Bind independently qualified capture settlement to exact original/current inventories.
71///
72/// Reuses current reservations, actual claims, bounded decoding and the canonical
73/// closed-baseline comparison. Every original ID and its metadata must remain unchanged.
74/// Applied names an explicitly attributed new ID; zero/one/many candidates infer no
75/// outcome. Negative evidence excludes transient capture/deletion. Unresolved needs
76/// an actually settled authenticated list; lost replies remain pending.
77///
78/// Integrations own original baseline chronology, actual authentication, freshness,
79/// read permission, attribution and custody. Matching digests establish none of those.
80/// This performs no IO, calls, serialization, receipt transition, retries, refunds,
81/// consistency/transfer qualification or terminal/fence/reference release admission.
82/// # Errors
83/// Rejects lifecycle/status lanes, changed reservations/claims/wire, baseline loss or
84/// drift, settlement identity drift and an Applied ID outside the exact new candidates.
85pub fn validate_capture_settlement<'a>(
86    request: &IcObservationRequest<'a>,
87    journal: &AttemptJournalRecord,
88    baseline: &'a IcSnapshotReply<'a>,
89    response: &'a IcObservationResponse,
90    challenge: &ArtifactChecksumRecord,
91    settlement: &'a IcCaptureSettlement,
92) -> Result<IcCaptureSettlementView<'a>, IcCaptureSettlementError> {
93    if request.mutation().method() != Method::TakeCanisterSnapshot
94        || request.payload().method() != Method::ListCanisterSnapshots
95    {
96        return Err(IcCaptureSettlementError::UnsupportedMethod);
97    }
98    let observation = validate_response(request, journal, response)?;
99    let IcObservationReplyView::Inventory(inventory) = observation.reply() else {
100        return Err(IcCaptureSettlementError::UnsupportedMethod);
101    };
102    let authority = request.authority().digest();
103    let baseline_digest = baseline.digest();
104    let inventory_digest = inventory.digest();
105    let candidates = validate_pair::<IcCaptureSettlementError>(
106        request.mutation().target(),
107        baseline,
108        inventory,
109        &InventorySettlementIdentity {
110            authority: &settlement.authority,
111            mutation_attempt: settlement.mutation_attempt,
112            observation_attempt: settlement.observation_attempt,
113            challenge: &settlement.challenge,
114            baseline: &settlement.baseline,
115            inventory: &settlement.inventory,
116            observation_evidence: &settlement.observation_evidence,
117        },
118        &InventorySettlementIdentity {
119            authority: &authority,
120            mutation_attempt: request.mutation_attempt(),
121            observation_attempt: request.observation_attempt(),
122            challenge,
123            baseline: &baseline_digest,
124            inventory: &inventory_digest,
125            observation_evidence: &response.input().evidence,
126        },
127    )?;
128    let (outcome, captured_snapshot) = admitted_outcome(
129        inventory,
130        &candidates,
131        InventoryAttribution::from(&settlement.attribution),
132    )
133    .map_err(|()| IcCaptureSettlementError::NotNewCandidate)?;
134    Ok(IcCaptureSettlementView {
135        observation,
136        baseline,
137        settlement,
138        captured_snapshot,
139        outcome,
140    })
141}
142
143/// Typed local claim denial; original allowances, obligations and references stay retained.
144#[derive(Debug, Error)]
145pub enum IcCaptureSettlementError {
146    /// Only original non-replacing capture and a reserved list observation are supported.
147    #[error("capture settlement requires original capture and list observation")]
148    UnsupportedMethod,
149    /// Existing association, current reservation or finite wire admission rejected.
150    #[error(transparent)]
151    Observation(#[from] IcObservationAssociationError),
152    /// Existing closed full-baseline comparison rejected.
153    #[error(transparent)]
154    Baseline(#[from] SnapshotInventoryDeltaError),
155    /// Complete original operation authority differs.
156    #[error("capture settlement original authority differs")]
157    AuthorityMismatch,
158    /// Original mutation or observation attempt differs.
159    #[error("capture settlement attempt identities differ")]
160    AttemptMismatch,
161    /// Caller-owned current qualification challenge differs.
162    #[error("capture settlement challenge differs")]
163    ChallengeMismatch,
164    /// Exact original baseline request/raw evidence differs.
165    #[error("capture settlement original baseline differs")]
166    BaselineMismatch,
167    /// Exact current inventory request/raw evidence differs.
168    #[error("capture settlement current inventory differs")]
169    InventoryMismatch,
170    /// Opaque observation evidence differs even if raw reply bytes match.
171    #[error("capture settlement observation evidence differs")]
172    ObservationEvidenceMismatch,
173    /// The attributed raw ID is not a bounded new candidate in the current inventory.
174    #[error("capture settlement ID is not a new candidate")]
175    NotNewCandidate,
176}
177
178impl From<InventorySettlementMismatch> for IcCaptureSettlementError {
179    fn from(value: InventorySettlementMismatch) -> Self {
180        match value {
181            InventorySettlementMismatch::Authority => Self::AuthorityMismatch,
182            InventorySettlementMismatch::Attempt => Self::AttemptMismatch,
183            InventorySettlementMismatch::Challenge => Self::ChallengeMismatch,
184            InventorySettlementMismatch::Baseline => Self::BaselineMismatch,
185            InventorySettlementMismatch::Inventory => Self::InventoryMismatch,
186            InventorySettlementMismatch::ObservationEvidence => Self::ObservationEvidenceMismatch,
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests;