ic_backup/model/ic_request/
mod.rs1mod method;
4pub use method::{IcManagementMethodRecord, IcRequestEffect};
5
6use crate::model::{artifacts::ArtifactChecksumRecord, attempt_journal::OperationBindingRecord};
7use candid::Principal;
8use ic_management_canister_types::{
9 CanisterIdRecord, LoadCanisterSnapshotArgs, TakeCanisterSnapshotArgs,
10};
11use serde::{Deserialize, Deserializer, Serialize, de};
12use std::fmt;
13use thiserror::Error;
14
15pub const MAX_IC_SNAPSHOT_ID_BYTES: usize = 256;
17pub const MAX_IC_ARGUMENT_BYTES: usize = 4096;
19pub const MAX_IC_REQUEST_RECORD_BYTES: u64 = 8192;
21
22#[derive(Clone, Debug)]
24pub struct IcManagementRequest {
25 pub method: IcManagementMethodRecord,
27 pub target: String,
29 pub snapshot_id: Option<Vec<u8>>,
31}
32
33#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
39#[serde(try_from = "RequestFields")]
40pub struct IcManagementRequestRecord {
41 version: u16,
42 method: IcManagementMethodRecord,
43 target: String,
44 snapshot_id: Option<Vec<u8>>,
45 #[serde(skip)]
46 target_bytes: Vec<u8>,
47 #[serde(skip)]
48 arguments: Vec<u8>,
49}
50#[derive(Deserialize)]
51#[serde(deny_unknown_fields)]
52struct RequestFields {
53 version: u16,
54 method: IcManagementMethodRecord,
55 target: String,
56 #[serde(deserialize_with = "required_snapshot")]
57 snapshot_id: Option<Vec<u8>>,
58}
59impl TryFrom<RequestFields> for IcManagementRequestRecord {
60 type Error = IcRequestError;
61 fn try_from(fields: RequestFields) -> Result<Self, Self::Error> {
62 if fields.version != 1 {
63 return Err(IcRequestError::UnsupportedVersion(fields.version));
64 }
65 Self::new(IcManagementRequest {
66 method: fields.method,
67 target: fields.target,
68 snapshot_id: fields.snapshot_id,
69 })
70 }
71}
72impl IcManagementRequestRecord {
73 pub fn new(request: IcManagementRequest) -> Result<Self, IcRequestError> {
82 let target = crate::model::principal::canonical_text(&request.target)
83 .ok_or(IcRequestError::InvalidTarget)?;
84 let principal = Principal::from_text(&target).map_err(|_| IcRequestError::InvalidTarget)?;
85 match (request.method, request.snapshot_id.as_deref()) {
86 (IcManagementMethodRecord::LoadCanisterSnapshot, None) => {
87 return Err(IcRequestError::SnapshotRequired);
88 }
89 (IcManagementMethodRecord::LoadCanisterSnapshot, Some(bytes)) => {
90 if bytes.is_empty() || bytes.len() > MAX_IC_SNAPSHOT_ID_BYTES {
91 return Err(IcRequestError::InvalidSnapshotId);
92 }
93 }
94 (_, Some(_)) => return Err(IcRequestError::UnexpectedSnapshot),
95 (_, None) => {}
96 }
97 let arguments = match request.method {
98 IcManagementMethodRecord::TakeCanisterSnapshot => {
99 candid::encode_one(TakeCanisterSnapshotArgs {
100 canister_id: principal,
101 replace_snapshot: None,
102 uninstall_code: Some(false),
103 sender_canister_version: None,
104 })
105 }
106 IcManagementMethodRecord::LoadCanisterSnapshot => {
107 candid::encode_one(LoadCanisterSnapshotArgs {
108 canister_id: principal,
109 snapshot_id: request
110 .snapshot_id
111 .clone()
112 .ok_or(IcRequestError::SnapshotRequired)?,
113 sender_canister_version: None,
114 })
115 }
116 IcManagementMethodRecord::CanisterStatus
117 | IcManagementMethodRecord::ListCanisterSnapshots
118 | IcManagementMethodRecord::StartCanister
119 | IcManagementMethodRecord::StopCanister => candid::encode_one(CanisterIdRecord {
120 canister_id: principal,
121 }),
122 }
123 .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
124 if arguments.len() > MAX_IC_ARGUMENT_BYTES {
125 return Err(IcRequestError::ArgumentsTooLarge);
126 }
127 Ok(Self {
128 version: 1,
129 method: request.method,
130 target,
131 snapshot_id: request.snapshot_id,
132 target_bytes: principal.as_slice().to_vec(),
133 arguments,
134 })
135 }
136 #[must_use]
138 pub const fn method(&self) -> IcManagementMethodRecord {
139 self.method
140 }
141 #[must_use]
143 pub fn target(&self) -> &str {
144 &self.target
145 }
146 #[must_use]
148 pub fn snapshot_id(&self) -> Option<&[u8]> {
149 self.snapshot_id.as_deref()
150 }
151 #[must_use]
153 pub fn arguments(&self) -> &[u8] {
154 &self.arguments
155 }
156 #[must_use]
158 pub const fn receiver(&self) -> &'static str {
159 "aaaaa-aa"
160 }
161 #[must_use]
166 pub fn digest(&self) -> ArtifactChecksumRecord {
167 management_request_digest(&self.target_bytes, self.method.name(), &self.arguments)
168 }
169 pub fn validate_mutation_binding(
175 &self,
176 binding: &OperationBindingRecord,
177 ) -> Result<(), IcRequestError> {
178 self.require_effect(IcRequestEffect::Mutation)?;
179 self.validate_identity(binding, binding.request())
180 }
181 pub fn validate_observation_binding(
188 &self,
189 binding: &OperationBindingRecord,
190 request: &ArtifactChecksumRecord,
191 ) -> Result<(), IcRequestError> {
192 self.require_effect(IcRequestEffect::Observation)?;
193 self.validate_identity(binding, request.hash())
194 }
195 fn require_effect(&self, expected: IcRequestEffect) -> Result<(), IcRequestError> {
196 if self.method.effect() != expected {
197 return Err(IcRequestError::EffectMismatch { expected });
198 }
199 Ok(())
200 }
201 fn validate_identity(
202 &self,
203 binding: &OperationBindingRecord,
204 expected: &str,
205 ) -> Result<(), IcRequestError> {
206 if self.target != binding.target() {
207 return Err(IcRequestError::TargetMismatch);
208 }
209 if self.digest().hash() != expected {
210 return Err(IcRequestError::DigestMismatch);
211 }
212 Ok(())
213 }
214}
215
216pub(super) fn management_request_digest(
218 target_bytes: &[u8],
219 method: &str,
220 arguments: &[u8],
221) -> ArtifactChecksumRecord {
222 let mut bytes = b"ic-backup/ic-management-request/v1\0".to_vec();
223 bytes.push(0); bytes.push(target_bytes.len().to_le_bytes()[0]); bytes.extend_from_slice(target_bytes);
226 bytes.push(1); bytes.push(method.len().to_le_bytes()[0]); bytes.extend_from_slice(method.as_bytes());
229 append_argument_length(&mut bytes, arguments.len());
230 bytes.extend_from_slice(arguments);
231 ArtifactChecksumRecord::from_bytes(&bytes)
232}
233
234#[expect(
235 clippy::cast_possible_truncation,
236 reason = "admitted argument byte length is at most 4096"
237)]
238fn append_argument_length(bytes: &mut Vec<u8>, length: usize) {
239 bytes.extend_from_slice(&(length as u32).to_be_bytes());
240}
241
242fn required_snapshot<'de, D: Deserializer<'de>>(
243 deserializer: D,
244) -> Result<Option<Vec<u8>>, D::Error> {
245 Ok(Option::<SnapshotBytes>::deserialize(deserializer)?.map(|bytes| bytes.0))
246}
247struct SnapshotBytes(Vec<u8>);
248impl<'de> Deserialize<'de> for SnapshotBytes {
249 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
250 struct BytesVisitor;
251 impl<'de> de::Visitor<'de> for BytesVisitor {
252 type Value = SnapshotBytes;
253 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 f.write_str("at most 256 exact snapshot bytes")
255 }
256 fn visit_seq<A: de::SeqAccess<'de>>(
257 self,
258 mut sequence: A,
259 ) -> Result<Self::Value, A::Error> {
260 let mut bytes = Vec::new();
261 while bytes.len() < MAX_IC_SNAPSHOT_ID_BYTES {
262 match sequence.next_element::<u8>()? {
263 Some(byte) => bytes.push(byte),
264 None => return Ok(SnapshotBytes(bytes)),
265 }
266 }
267 if sequence.next_element::<de::IgnoredAny>()?.is_some() {
268 return Err(de::Error::custom(IcRequestError::InvalidSnapshotId));
269 }
270 Ok(SnapshotBytes(bytes))
271 }
272 }
273 deserializer.deserialize_seq(BytesVisitor)
274 }
275}
276
277#[derive(Debug, Error, Eq, PartialEq)]
279pub enum IcRequestError {
280 #[error("unsupported IC request version {0}")]
282 UnsupportedVersion(u16),
283 #[error("invalid IC request target principal")]
285 InvalidTarget,
286 #[error("load snapshot request requires snapshot_id")]
288 SnapshotRequired,
289 #[error("snapshot_id is not admitted for this IC request method")]
291 UnexpectedSnapshot,
292 #[error("snapshot_id must contain 1..={MAX_IC_SNAPSHOT_ID_BYTES} raw bytes")]
294 InvalidSnapshotId,
295 #[error("IC request Candid encoding failed: {0}")]
297 Encoding(String),
298 #[error("IC request arguments exceed {MAX_IC_ARGUMENT_BYTES} bytes")]
300 ArgumentsTooLarge,
301 #[error("IC request must have effect class {expected:?}")]
303 EffectMismatch {
304 expected: IcRequestEffect,
306 },
307 #[error("IC request target differs from original binding")]
309 TargetMismatch,
310 #[error("IC request digest differs from original binding")]
312 DigestMismatch,
313}
314
315#[cfg(test)]
316mod tests;