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