Skip to main content

ic_backup/policy/ic_observation/settlement/
mod.rs

1//! Pure admission of independently qualified original lifecycle settlement.
2
3use super::{
4    IcObservationAssociationError, IcObservationReplyView, IcObservationResponseView,
5    validate_response,
6};
7use crate::model::{
8    artifacts::ArtifactChecksumRecord,
9    attempt_journal::{AttemptJournalRecord, ObservationOutcomeRecord},
10    ic_observation::{
11        IcLifecycleAttribution, IcLifecycleSettlement, IcObservationRequest, IcObservationResponse,
12    },
13    ic_request::IcManagementMethodRecord as Method,
14};
15use crate::policy::settlement_identity::{SettlementIdentity, SettlementIdentityMismatch};
16use thiserror::Error;
17
18/// Matched passive original attribution, never an authenticated receipt.
19#[derive(Debug)]
20pub struct IcLifecycleSettlementView<'a> {
21    observation: IcObservationResponseView<'a>,
22    settlement: &'a IcLifecycleSettlement,
23    outcome: ObservationOutcomeRecord,
24}
25impl<'a> IcLifecycleSettlementView<'a> {
26    /// Read the independently claimed outcome; integration qualification precedes recording.
27    #[must_use]
28    pub const fn outcome(&self) -> ObservationOutcomeRecord {
29        self.outcome
30    }
31    /// Read exact bounded status association under original reservations.
32    #[must_use]
33    pub const fn observation(&self) -> &IcObservationResponseView<'a> {
34        &self.observation
35    }
36    /// Read original attribution and retained qualification evidence.
37    #[must_use]
38    pub const fn settlement(&self) -> &'a IcLifecycleSettlement {
39        self.settlement
40    }
41}
42
43/// Bind independently qualified stop/start/load settlement to the original status evidence.
44///
45/// Reuses current reservation/actual claim checks and the bounded status decoder.
46/// Current status, controllers and module hashes never infer any outcome. Applied
47/// load claims independently qualify exact restored state; desired status does not
48/// identify the original request. Opposite state cannot exclude transient application.
49/// Lost replies cannot enter this successful-observation boundary as uncertainty.
50///
51/// This matches passive claims only. Authentication, chronology, attribution,
52/// freshness, read permission and command custody remain integration-owned. Only
53/// that integration explicitly records the existing journal receipt. No IO, calls,
54/// automatic transitions, retries, refunds, restart or terminal/release admission occur.
55/// # Errors
56/// Rejects capture/inventory lanes, changed reservations/actual claims, invalid wire,
57/// different authority/attempts, challenge or exact retained status/evidence identity.
58pub fn validate_lifecycle_settlement<'a>(
59    request: &IcObservationRequest<'a>,
60    journal: &AttemptJournalRecord,
61    response: &'a IcObservationResponse,
62    challenge: &ArtifactChecksumRecord,
63    settlement: &'a IcLifecycleSettlement,
64) -> Result<IcLifecycleSettlementView<'a>, IcLifecycleSettlementError> {
65    if !matches!(
66        request.mutation().method(),
67        Method::StopCanister | Method::StartCanister | Method::LoadCanisterSnapshot
68    ) || request.payload().method() != Method::CanisterStatus
69    {
70        return Err(IcLifecycleSettlementError::UnsupportedMethod);
71    }
72    let observation = validate_response(request, journal, response)?;
73    SettlementIdentity {
74        authority: &settlement.authority,
75        mutation_attempt: settlement.mutation_attempt,
76        observation_attempt: settlement.observation_attempt,
77        challenge: &settlement.challenge,
78    }
79    .compare(&SettlementIdentity {
80        authority: &request.authority().digest(),
81        mutation_attempt: request.mutation_attempt(),
82        observation_attempt: request.observation_attempt(),
83        challenge,
84    })?;
85    let IcObservationReplyView::Status(status) = observation.reply() else {
86        return Err(IcLifecycleSettlementError::UnsupportedMethod);
87    };
88    if settlement.status != status.digest() {
89        return Err(IcLifecycleSettlementError::StatusMismatch);
90    }
91    if settlement.observation_evidence != response.input().evidence {
92        return Err(IcLifecycleSettlementError::ObservationEvidenceMismatch);
93    }
94    let outcome = match &settlement.attribution {
95        IcLifecycleAttribution::Applied { .. } => ObservationOutcomeRecord::Applied,
96        IcLifecycleAttribution::NotApplied { .. } => ObservationOutcomeRecord::NotApplied,
97        IcLifecycleAttribution::Unresolved { .. } => ObservationOutcomeRecord::Uncertain,
98    };
99    Ok(IcLifecycleSettlementView {
100        observation,
101        settlement,
102        outcome,
103    })
104}
105
106/// Typed local claim denial; original allowances, obligations and references remain retained.
107#[derive(Debug, Error)]
108pub enum IcLifecycleSettlementError {
109    /// Only original stop/start/load with a separately reserved status call is supported.
110    #[error("lifecycle settlement requires original stop/start/load and status observation")]
111    UnsupportedMethod,
112    /// Existing association, current reservation or bounded decoder rejected.
113    #[error(transparent)]
114    Observation(#[from] IcObservationAssociationError),
115    /// Complete original operation authority differs.
116    #[error("lifecycle settlement original authority differs")]
117    AuthorityMismatch,
118    /// Original mutation or observation attempt differs.
119    #[error("lifecycle settlement attempt identities differ")]
120    AttemptMismatch,
121    /// Caller-owned current qualification challenge differs.
122    #[error("lifecycle settlement challenge differs")]
123    ChallengeMismatch,
124    /// Exact original status request/raw reply digest differs.
125    #[error("lifecycle settlement status evidence differs")]
126    StatusMismatch,
127    /// Retained opaque observation evidence differs even if reply bytes match.
128    #[error("lifecycle settlement observation evidence differs")]
129    ObservationEvidenceMismatch,
130}
131
132impl From<SettlementIdentityMismatch> for IcLifecycleSettlementError {
133    fn from(value: SettlementIdentityMismatch) -> Self {
134        match value {
135            SettlementIdentityMismatch::Authority => Self::AuthorityMismatch,
136            SettlementIdentityMismatch::Attempt => Self::AttemptMismatch,
137            SettlementIdentityMismatch::Challenge => Self::ChallengeMismatch,
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests;