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