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::{IcSnapshotInfo, 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    // The view owns its decoded inventory. Settlement keeps an admitted index
45    // and borrows the exact descriptor through this owner when requested.
46    pub(super) fn inventory_snapshot(&self, index: usize) -> Option<&IcSnapshotInfo> {
47        match &self.reply {
48            IcObservationReplyView::Inventory(reply) => reply.snapshots().get(index),
49            IcObservationReplyView::Status(_) => None,
50        }
51    }
52}
53
54/// Match current original reservations and actual claims, then reuse existing decoders.
55///
56/// This performs no IO, dispatch or receipt transition. Successful wire association
57/// proves no actual authentication, chronology, freshness, exclusive capture attribution,
58/// lifecycle equivalence, load success, application safety or permission. Zero/one/many
59/// snapshots and Stopped/controller projections never automatically settle a mutation.
60/// Lost replies retain the pending observation; they cannot mean settled Uncertain.
61/// # Errors
62/// Rejects changed reservations/authority/attempts/bytes/context/target and invalid wire.
63pub fn validate_response<'a>(
64    request: &IcObservationRequest<'a>,
65    journal: &AttemptJournalRecord,
66    response: &'a IcObservationResponse,
67) -> Result<IcObservationResponseView<'a>, IcObservationAssociationError> {
68    request.validate_journal(journal)?;
69    validate_association(
70        &ObservationAssociation {
71            authority: request.authority(),
72            mutation_attempt: request.mutation_attempt(),
73            observation_attempt: request.observation_attempt(),
74            payload: request.payload(),
75            context: request.plan().context(),
76        },
77        response,
78    )
79}
80
81pub(crate) struct ObservationAssociation<'binding, 'payload> {
82    pub authority: &'binding AttemptAuthorityRecord,
83    pub mutation_attempt: u32,
84    pub observation_attempt: u32,
85    pub payload: &'payload IcManagementRequestRecord,
86    pub context: &'binding PlanContextRecord,
87}
88
89pub(crate) fn validate_association<'a>(
90    binding: &ObservationAssociation<'_, 'a>,
91    response: &'a IcObservationResponse,
92) -> Result<IcObservationResponseView<'a>, IcObservationAssociationError> {
93    validate_claims(
94        &ObservationClaims {
95            authority: binding.authority,
96            mutation_attempt: binding.mutation_attempt,
97            observation_attempt: binding.observation_attempt,
98            request: binding.payload.digest(),
99            context: binding.context,
100            target: binding.payload.target(),
101        },
102        response.input(),
103    )?;
104    let reply = match binding.payload.method() {
105        IcManagementMethodRecord::ListCanisterSnapshots => IcObservationReplyView::Inventory(
106            IcSnapshotReply::decode(binding.payload, &response.input().reply)?,
107        ),
108        // The sealed request excludes mutations. Status stays with its existing owner.
109        _ => IcObservationReplyView::Status(IcLifecycleReply::decode(
110            binding.payload,
111            &response.input().reply,
112        )?),
113    };
114    Ok(IcObservationResponseView { response, reply })
115}
116
117pub(crate) struct ObservationClaims<'a> {
118    pub authority: &'a AttemptAuthorityRecord,
119    pub mutation_attempt: u32,
120    pub observation_attempt: u32,
121    pub request: ArtifactChecksumRecord,
122    pub context: &'a PlanContextRecord,
123    pub target: &'a str,
124}
125
126pub(crate) fn validate_claims(
127    binding: &ObservationClaims<'_>,
128    input: &IcObservationResponseInput,
129) -> Result<(), IcObservationAssociationError> {
130    if input.authority != binding.authority.digest() {
131        return Err(IcObservationAssociationError::AuthorityMismatch);
132    }
133    if input.mutation_attempt != binding.mutation_attempt
134        || input.observation_attempt != binding.observation_attempt
135    {
136        return Err(IcObservationAssociationError::AttemptMismatch);
137    }
138    if input.request != binding.request {
139        return Err(IcObservationAssociationError::RequestMismatch);
140    }
141    if &input.context != binding.context {
142        return Err(IcObservationAssociationError::ContextMismatch);
143    }
144    if input.target != binding.target {
145        return Err(IcObservationAssociationError::TargetMismatch);
146    }
147    Ok(())
148}
149
150/// Typed passive association denial; all original spending/obligations remain retained.
151#[derive(Debug, Error)]
152pub enum IcObservationAssociationError {
153    /// Current original journal no longer matches the request.
154    #[error(transparent)]
155    Reservation(#[from] IcObservationRequestError),
156    /// Another original operation authority was claimed.
157    #[error("IC observation response authority mismatch")]
158    AuthorityMismatch,
159    /// Another original mutation or observation attempt was claimed.
160    #[error("IC observation response attempt mismatch")]
161    AttemptMismatch,
162    /// Another exact observation payload digest was claimed.
163    #[error("IC observation response request mismatch")]
164    RequestMismatch,
165    /// Actual claimed network/caller/release differs.
166    #[error("IC observation response context mismatch")]
167    ContextMismatch,
168    /// Actual claimed response target differs.
169    #[error("IC observation response target mismatch")]
170    TargetMismatch,
171    /// Existing inventory decoder rejected the reply.
172    #[error(transparent)]
173    Inventory(#[from] IcSnapshotReplyError),
174    /// Existing status decoder rejected the reply.
175    #[error(transparent)]
176    Status(#[from] IcLifecycleReplyError),
177}
178
179mod capture_settlement;
180pub use capture_settlement::{
181    IcCaptureSettlementError, IcCaptureSettlementView, validate_capture_settlement,
182};
183
184mod settlement;
185pub use settlement::{
186    IcLifecycleSettlementError, IcLifecycleSettlementView, validate_lifecycle_settlement,
187};
188
189#[cfg(test)]
190mod tests;