ic_backup/model/ic_lifecycle_reply/mod.rs
1//! Bounded lifecycle acknowledgements and status/controller projections.
2
3mod wire;
4
5use super::{
6 artifacts::ArtifactChecksumRecord,
7 control_authority::{ControlObservationError, ControllerSet},
8 ic_request::{IcManagementMethodRecord, IcManagementRequestRecord},
9};
10use ic_management_canister_types::CanisterStatusType;
11use thiserror::Error;
12
13/// Maximum raw reply bytes, checked before any status decoding.
14pub const MAX_IC_LIFECYCLE_REPLY_BYTES: usize = 1024 * 1024;
15
16/// Read-only required status and complete declared controller set.
17///
18/// Other upstream status fields are skipped under finite work limits; their
19/// presence, values and semantics are not qualified by this projection. A decoded
20/// Stopped value establishes no drained work, continuous fence or load success.
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct IcCanisterStatusInfo {
23 status: CanisterStatusType,
24 controllers: ControllerSet,
25}
26
27impl IcCanisterStatusInfo {
28 /// Read the upstream running, stopping or stopped variant exactly.
29 #[must_use]
30 pub const fn status(&self) -> CanisterStatusType {
31 self.status
32 }
33
34 /// Read the canonical bounded set through its existing owning boundary.
35 ///
36 /// An explicitly empty set stays empty; missing settings/controllers reject.
37 #[must_use]
38 pub const fn controllers(&self) -> &ControllerSet {
39 &self.controllers
40 }
41}
42
43/// Decoded wire shape, with no receipt or current authority admission.
44#[derive(Debug, Eq, PartialEq)]
45pub enum IcLifecycleReplyKind {
46 /// Required status/controller projection of a canister-status result.
47 Status(IcCanisterStatusInfo),
48 /// Exact empty Candid argument tuple for stop, start or snapshot load.
49 ///
50 /// This is local wire admission, not authentication or ongoing lifecycle state.
51 Acknowledgement,
52}
53
54/// Local reply evidence associated with the caller's immutable exact request.
55///
56/// The wire carries no network, caller, target or challenge. Integrations must
57/// authenticate association, qualify observation timing and consume original
58/// per-call authority. No decoded kind settles pending attempts, replenishes limits,
59/// admits restart/load/fence release or proves complete same-release restoration.
60#[derive(Debug)]
61pub struct IcLifecycleReply<'request> {
62 request: &'request IcManagementRequestRecord,
63 kind: IcLifecycleReplyKind,
64 payload_checksum: ArtifactChecksumRecord,
65}
66
67impl<'request> IcLifecycleReply<'request> {
68 /// Admit the existing status, stop, start or load reply under finite local bounds.
69 ///
70 /// Status requires exactly one value with required status/settings/controllers.
71 /// Unprojected fields are skipped, not validated. Acknowledgements require the
72 /// canonical six-byte empty tuple, with no extra type table, argument or bytes.
73 ///
74 /// # Errors
75 /// Rejects unsupported methods, excessive raw input, invalid/bounded wire shapes
76 /// and duplicate controller identity, with no raw payload in diagnostics.
77 pub fn decode(
78 request: &'request IcManagementRequestRecord,
79 bytes: &[u8],
80 ) -> Result<Self, IcLifecycleReplyError> {
81 let method = request.method();
82 if !matches!(
83 method,
84 IcManagementMethodRecord::CanisterStatus
85 | IcManagementMethodRecord::StopCanister
86 | IcManagementMethodRecord::StartCanister
87 | IcManagementMethodRecord::LoadCanisterSnapshot
88 ) {
89 return Err(IcLifecycleReplyError::UnsupportedMethod { method });
90 }
91 if bytes.len() > MAX_IC_LIFECYCLE_REPLY_BYTES {
92 return Err(IcLifecycleReplyError::ReplyTooLarge);
93 }
94 let kind = if method == IcManagementMethodRecord::CanisterStatus {
95 IcLifecycleReplyKind::Status(wire::status(bytes)?)
96 } else {
97 if bytes != b"DIDL\0\0" {
98 return Err(IcLifecycleReplyError::InvalidReply);
99 }
100 IcLifecycleReplyKind::Acknowledgement
101 };
102 Ok(Self {
103 request,
104 kind,
105 payload_checksum: ArtifactChecksumRecord::from_bytes(bytes),
106 })
107 }
108
109 /// Read the original method, canonical target and exact wire identity owner.
110 #[must_use]
111 pub const fn request(&self) -> &'request IcManagementRequestRecord {
112 self.request
113 }
114
115 /// Read the method-specific local projection or acknowledgement.
116 #[must_use]
117 pub const fn kind(&self) -> &IcLifecycleReplyKind {
118 &self.kind
119 }
120
121 /// Read SHA-256 of exact raw bytes, including all unprojected fields/order.
122 #[must_use]
123 pub const fn payload_checksum(&self) -> &ArtifactChecksumRecord {
124 &self.payload_checksum
125 }
126
127 /// Hash existing request digest and exact raw-reply checksum in a v1 domain.
128 ///
129 /// Two fixed 64-byte lowercase SHA-256 strings follow the NUL-terminated domain.
130 /// This binds declared association and never authenticates a transport receipt.
131 #[must_use]
132 pub fn digest(&self) -> ArtifactChecksumRecord {
133 let mut bytes = b"ic-backup/ic-lifecycle-reply/v1\0".to_vec();
134 bytes.extend_from_slice(self.request.digest().hash().as_bytes());
135 bytes.extend_from_slice(self.payload_checksum.hash().as_bytes());
136 ArtifactChecksumRecord::from_bytes(&bytes)
137 }
138}
139
140/// Typed local wire admission failure, preserving caller-owned evidence and journals.
141#[derive(Debug, Error, Eq, PartialEq)]
142pub enum IcLifecycleReplyError {
143 /// The existing capture/inventory shapes have a separate reply owner.
144 #[error("lifecycle reply codec does not support {method:?}")]
145 UnsupportedMethod {
146 /// Actual request method.
147 method: IcManagementMethodRecord,
148 },
149 /// Raw input exceeded the bound before parsing.
150 #[error("lifecycle reply exceeds {MAX_IC_LIFECYCLE_REPLY_BYTES} bytes")]
151 ReplyTooLarge,
152 /// Required shape/fields/counts, decoder work or exact consumption failed.
153 #[error("invalid or unbounded lifecycle Candid reply")]
154 InvalidReply,
155 /// Canonical controller admission failed in its existing owner.
156 #[error(transparent)]
157 Controllers(#[from] ControlObservationError),
158}
159
160#[cfg(test)]
161mod tests;