Skip to main content

ic_backup/policy/ic_observation/
mod.rs

1//! Pure reserved IC observation association and passive qualified original effect claims.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    attempt_journal::{AttemptAuthorityRecord, AttemptJournalRecord},
6    ic_lifecycle_reply::{IcLifecycleReply, IcLifecycleReplyError},
7    ic_observation::{
8        IcObservationRequest, IcObservationRequestError, IcObservationResponse,
9        IcObservationResponseInput,
10    },
11    ic_request::{IcManagementMethodRecord, IcManagementRequestRecord},
12    ic_snapshot_reply::{IcSnapshotReply, IcSnapshotReplyError},
13    operation_plan::PlanContextRecord,
14};
15use thiserror::Error;
16
17/// Existing method-specific wire projections; no fresh permission or effect attribution.
18#[derive(Debug)]
19pub enum IcObservationReplyView<'a> {
20    /// Canonical bounded snapshot inventory with unchanged raw identities/metadata.
21    Inventory(IcSnapshotReply<'a>),
22    /// Required status/controller projection; Stopped alone proves no drain or load outcome.
23    Status(IcLifecycleReply<'a>),
24}
25
26/// Read-only association to exact original reserved observation evidence.
27#[derive(Debug)]
28pub struct IcObservationResponseView<'a> {
29    response: &'a IcObservationResponse,
30    reply: IcObservationReplyView<'a>,
31}
32impl<'a> IcObservationResponseView<'a> {
33    /// Read immutable original claims and exact raw response evidence.
34    #[must_use]
35    pub const fn response(&self) -> &'a IcObservationResponse {
36        self.response
37    }
38    /// Read the existing decoder's bounded method-specific projection.
39    #[must_use]
40    pub const fn reply(&self) -> &IcObservationReplyView<'a> {
41        &self.reply
42    }
43}
44
45/// Match current original reservations and actual claims, then reuse existing decoders.
46///
47/// This performs no IO, dispatch or receipt transition. Successful wire association
48/// proves no actual authentication, chronology, freshness, exclusive capture attribution,
49/// lifecycle equivalence, load success, application safety or permission. Zero/one/many
50/// snapshots and Stopped/controller projections never automatically settle a mutation.
51/// Lost replies retain the pending observation; they cannot mean settled Uncertain.
52/// # Errors
53/// Rejects changed reservations/authority/attempts/bytes/context/target and invalid wire.
54pub fn validate_response<'a>(
55    request: &IcObservationRequest<'a>,
56    journal: &AttemptJournalRecord,
57    response: &'a IcObservationResponse,
58) -> Result<IcObservationResponseView<'a>, IcObservationAssociationError> {
59    request.validate_journal(journal)?;
60    validate_association(
61        &ObservationAssociation {
62            authority: request.authority(),
63            mutation_attempt: request.mutation_attempt(),
64            observation_attempt: request.observation_attempt(),
65            payload: request.payload(),
66            context: request.plan().context(),
67        },
68        response,
69    )
70}
71
72pub(crate) struct ObservationAssociation<'binding, 'payload> {
73    pub authority: &'binding AttemptAuthorityRecord,
74    pub mutation_attempt: u32,
75    pub observation_attempt: u32,
76    pub payload: &'payload IcManagementRequestRecord,
77    pub context: &'binding PlanContextRecord,
78}
79
80pub(crate) fn validate_association<'a>(
81    binding: &ObservationAssociation<'_, 'a>,
82    response: &'a IcObservationResponse,
83) -> Result<IcObservationResponseView<'a>, IcObservationAssociationError> {
84    validate_claims(
85        &ObservationClaims {
86            authority: binding.authority,
87            mutation_attempt: binding.mutation_attempt,
88            observation_attempt: binding.observation_attempt,
89            request: binding.payload.digest(),
90            context: binding.context,
91            target: binding.payload.target(),
92        },
93        response.input(),
94    )?;
95    let reply = match binding.payload.method() {
96        IcManagementMethodRecord::ListCanisterSnapshots => IcObservationReplyView::Inventory(
97            IcSnapshotReply::decode(binding.payload, &response.input().reply)?,
98        ),
99        // The sealed request excludes mutations. Status stays with its existing owner.
100        _ => IcObservationReplyView::Status(IcLifecycleReply::decode(
101            binding.payload,
102            &response.input().reply,
103        )?),
104    };
105    Ok(IcObservationResponseView { response, reply })
106}
107
108pub(crate) struct ObservationClaims<'a> {
109    pub authority: &'a AttemptAuthorityRecord,
110    pub mutation_attempt: u32,
111    pub observation_attempt: u32,
112    pub request: ArtifactChecksumRecord,
113    pub context: &'a PlanContextRecord,
114    pub target: &'a str,
115}
116
117pub(crate) fn validate_claims(
118    binding: &ObservationClaims<'_>,
119    input: &IcObservationResponseInput,
120) -> Result<(), IcObservationAssociationError> {
121    if input.authority != binding.authority.digest() {
122        return Err(IcObservationAssociationError::AuthorityMismatch);
123    }
124    if input.mutation_attempt != binding.mutation_attempt
125        || input.observation_attempt != binding.observation_attempt
126    {
127        return Err(IcObservationAssociationError::AttemptMismatch);
128    }
129    if input.request != binding.request {
130        return Err(IcObservationAssociationError::RequestMismatch);
131    }
132    if &input.context != binding.context {
133        return Err(IcObservationAssociationError::ContextMismatch);
134    }
135    if input.target != binding.target {
136        return Err(IcObservationAssociationError::TargetMismatch);
137    }
138    Ok(())
139}
140
141/// Typed passive association denial; all original spending/obligations remain retained.
142#[derive(Debug, Error)]
143pub enum IcObservationAssociationError {
144    /// Current original journal no longer matches the request.
145    #[error(transparent)]
146    Reservation(#[from] IcObservationRequestError),
147    /// Another original operation authority was claimed.
148    #[error("IC observation response authority mismatch")]
149    AuthorityMismatch,
150    /// Another original mutation or observation attempt was claimed.
151    #[error("IC observation response attempt mismatch")]
152    AttemptMismatch,
153    /// Another exact observation payload digest was claimed.
154    #[error("IC observation response request mismatch")]
155    RequestMismatch,
156    /// Actual claimed network/caller/release differs.
157    #[error("IC observation response context mismatch")]
158    ContextMismatch,
159    /// Actual claimed response target differs.
160    #[error("IC observation response target mismatch")]
161    TargetMismatch,
162    /// Existing inventory decoder rejected the reply.
163    #[error(transparent)]
164    Inventory(#[from] IcSnapshotReplyError),
165    /// Existing status decoder rejected the reply.
166    #[error(transparent)]
167    Status(#[from] IcLifecycleReplyError),
168}
169
170mod capture_settlement;
171pub use capture_settlement::{
172    IcCaptureSettlementError, IcCaptureSettlementView, validate_capture_settlement,
173};
174
175mod settlement;
176pub use settlement::{
177    IcLifecycleSettlementError, IcLifecycleSettlementView, validate_lifecycle_settlement,
178};
179
180#[cfg(test)]
181mod tests;