Skip to main content

ic_backup/policy/ic_observation/
mod.rs

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