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 let mut bytes = b"ic-backup/ic-management-request/v1\0".to_vec();
168 bytes.push(0); bytes.push(self.target_bytes.len().to_le_bytes()[0]); bytes.extend_from_slice(&self.target_bytes);
171 bytes.push(1); let method = self.method.name().as_bytes();
173 bytes.push(method.len().to_le_bytes()[0]); bytes.extend_from_slice(method);
175 append_argument_length(&mut bytes, self.arguments.len());
176 bytes.extend_from_slice(&self.arguments);
177 ArtifactChecksumRecord::from_bytes(&bytes)
178 }
179 pub fn validate_mutation_binding(
185 &self,
186 binding: &OperationBindingRecord,
187 ) -> Result<(), IcRequestError> {
188 self.require_effect(IcRequestEffect::Mutation)?;
189 self.validate_identity(binding, binding.request())
190 }
191 pub fn validate_observation_binding(
198 &self,
199 binding: &OperationBindingRecord,
200 request: &ArtifactChecksumRecord,
201 ) -> Result<(), IcRequestError> {
202 self.require_effect(IcRequestEffect::Observation)?;
203 self.validate_identity(binding, request.hash())
204 }
205 fn require_effect(&self, expected: IcRequestEffect) -> Result<(), IcRequestError> {
206 if self.method.effect() != expected {
207 return Err(IcRequestError::EffectMismatch { expected });
208 }
209 Ok(())
210 }
211 fn validate_identity(
212 &self,
213 binding: &OperationBindingRecord,
214 expected: &str,
215 ) -> Result<(), IcRequestError> {
216 if self.target != binding.target() {
217 return Err(IcRequestError::TargetMismatch);
218 }
219 if self.digest().hash() != expected {
220 return Err(IcRequestError::DigestMismatch);
221 }
222 Ok(())
223 }
224}
225
226#[expect(
227 clippy::cast_possible_truncation,
228 reason = "admitted argument byte length is at most 4096"
229)]
230fn append_argument_length(bytes: &mut Vec<u8>, length: usize) {
231 bytes.extend_from_slice(&(length as u32).to_be_bytes());
232}
233
234fn required_snapshot<'de, D: Deserializer<'de>>(
235 deserializer: D,
236) -> Result<Option<Vec<u8>>, D::Error> {
237 Ok(Option::<SnapshotBytes>::deserialize(deserializer)?.map(|bytes| bytes.0))
238}
239struct SnapshotBytes(Vec<u8>);
240impl<'de> Deserialize<'de> for SnapshotBytes {
241 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
242 struct BytesVisitor;
243 impl<'de> de::Visitor<'de> for BytesVisitor {
244 type Value = SnapshotBytes;
245 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 f.write_str("at most 256 exact snapshot bytes")
247 }
248 fn visit_seq<A: de::SeqAccess<'de>>(
249 self,
250 mut sequence: A,
251 ) -> Result<Self::Value, A::Error> {
252 let mut bytes = Vec::new();
253 while bytes.len() < MAX_IC_SNAPSHOT_ID_BYTES {
254 match sequence.next_element::<u8>()? {
255 Some(byte) => bytes.push(byte),
256 None => return Ok(SnapshotBytes(bytes)),
257 }
258 }
259 if sequence.next_element::<de::IgnoredAny>()?.is_some() {
260 return Err(de::Error::custom(IcRequestError::InvalidSnapshotId));
261 }
262 Ok(SnapshotBytes(bytes))
263 }
264 }
265 deserializer.deserialize_seq(BytesVisitor)
266 }
267}
268
269#[derive(Debug, Error, Eq, PartialEq)]
271pub enum IcRequestError {
272 #[error("unsupported IC request version {0}")]
274 UnsupportedVersion(u16),
275 #[error("invalid IC request target principal")]
277 InvalidTarget,
278 #[error("load snapshot request requires snapshot_id")]
280 SnapshotRequired,
281 #[error("snapshot_id is not admitted for this IC request method")]
283 UnexpectedSnapshot,
284 #[error("snapshot_id must contain 1..={MAX_IC_SNAPSHOT_ID_BYTES} raw bytes")]
286 InvalidSnapshotId,
287 #[error("IC request Candid encoding failed: {0}")]
289 Encoding(String),
290 #[error("IC request arguments exceed {MAX_IC_ARGUMENT_BYTES} bytes")]
292 ArgumentsTooLarge,
293 #[error("IC request must have effect class {expected:?}")]
295 EffectMismatch {
296 expected: IcRequestEffect,
298 },
299 #[error("IC request target differs from original binding")]
301 TargetMismatch,
302 #[error("IC request digest differs from original binding")]
304 DigestMismatch,
305}
306
307#[cfg(test)]
308mod tests;