ic_backup/model/ic_snapshot_upload/
mod.rs1mod attempt;
4mod plan;
5mod reply;
6pub(crate) use attempt::original_authority;
7pub use attempt::{IcSnapshotUploadAttempt, IcSnapshotUploadAttemptError};
8pub use plan::{IcSnapshotDataUploadPlan, IcSnapshotDataUploadPlanningError};
9pub use reply::{IcSnapshotUploadReply, IcSnapshotUploadReplyKind};
10
11use super::{
12 artifacts::{ArtifactChecksumRecord, ChecksumError},
13 ic_request::{IcRequestError, MAX_IC_SNAPSHOT_ID_BYTES, management_request_digest},
14 ic_snapshot_data::{IcSnapshotDataError, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES, validate_kind},
15 ic_snapshot_metadata::IcSnapshotMetadataReply,
16 operation_plan::OperationPlanRecord,
17};
18use candid::Principal;
19use ic_management_canister_types::{
20 SnapshotDataKind, SnapshotDataOffset, UploadCanisterSnapshotDataArgs,
21 UploadCanisterSnapshotMetadataArgs,
22};
23use std::fmt;
24use thiserror::Error;
25
26pub const MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES: usize = 2 * 1024 * 1024;
28pub const MAX_IC_SNAPSHOT_UPLOAD_REPLY_BYTES: usize = 4096;
30
31#[derive(Debug)]
33pub enum IcSnapshotUploadKind {
34 Metadata,
36 Data {
38 snapshot_id: Vec<u8>,
40 source_kind: SnapshotDataKind,
42 chunk_checksum: ArtifactChecksumRecord,
44 metadata_request: ArtifactChecksumRecord,
46 },
47}
48
49pub struct IcSnapshotUploadRequest<'source> {
57 source_plan: &'source OperationPlanRecord,
58 source: &'source IcSnapshotMetadataReply<'source>,
59 source_checksum: ArtifactChecksumRecord,
60 kind: IcSnapshotUploadKind,
61 arguments: Vec<u8>,
62 target_bytes: Vec<u8>,
63}
64
65impl<'source> IcSnapshotUploadRequest<'source> {
66 pub fn metadata(
75 source_plan: &'source OperationPlanRecord,
76 source: &'source IcSnapshotMetadataReply<'source>,
77 source_checksum: &ArtifactChecksumRecord,
78 ) -> Result<Self, IcSnapshotUploadError> {
79 if !source_plan
80 .selected_targets()
81 .iter()
82 .any(|target| target == source.request().target())
83 {
84 return Err(IcSnapshotUploadError::SourceTargetMismatch);
85 }
86 let values = source.metadata();
87 let globals = values
88 .globals
89 .iter()
90 .cloned()
91 .collect::<Option<Vec<_>>>()
92 .ok_or(IcSnapshotUploadError::UnavailableGlobal)?;
93 let target = Principal::from_text(source.request().target())
94 .map_err(|_| IcRequestError::InvalidTarget)?;
95 let arguments = candid::encode_one(UploadCanisterSnapshotMetadataArgs {
96 canister_id: target,
97 replace_snapshot: None,
98 wasm_module_size: values.wasm_module_size,
99 globals,
100 wasm_memory_size: values.wasm_memory_size,
101 stable_memory_size: values.stable_memory_size,
102 certified_data: values.certified_data.clone(),
103 global_timer: values.global_timer.clone(),
104 on_low_wasm_memory_hook_status: values.on_low_wasm_memory_hook_status.clone(),
105 })
106 .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
107 Self::from_arguments(
108 source_plan,
109 source,
110 source_checksum,
111 IcSnapshotUploadKind::Metadata,
112 arguments,
113 )
114 }
115
116 pub fn data(
127 metadata: &Self,
128 snapshot_id: &[u8],
129 source_kind: SnapshotDataKind,
130 chunk: &[u8],
131 ) -> Result<Self, IcSnapshotUploadError> {
132 metadata.validate_data_destination(snapshot_id)?;
133 validate_kind(metadata.source, &source_kind)?;
134 if chunk.len() > MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES {
135 return Err(IcSnapshotUploadError::ChunkTooLarge);
136 }
137 let checksum = ArtifactChecksumRecord::from_bytes(chunk);
138 let kind = match &source_kind {
139 SnapshotDataKind::WasmModule { offset, size } => {
140 require_length(chunk, *size)?;
141 SnapshotDataOffset::WasmModule { offset: *offset }
142 }
143 SnapshotDataKind::WasmMemory { offset, size } => {
144 require_length(chunk, *size)?;
145 SnapshotDataOffset::WasmMemory { offset: *offset }
146 }
147 SnapshotDataKind::StableMemory { offset, size } => {
148 require_length(chunk, *size)?;
149 SnapshotDataOffset::StableMemory { offset: *offset }
150 }
151 SnapshotDataKind::WasmChunk { hash } => {
152 checksum.verify(&crate::hash::hex_bytes(hash))?;
153 SnapshotDataOffset::WasmChunk
154 }
155 };
156 let target =
157 Principal::from_text(metadata.target()).map_err(|_| IcRequestError::InvalidTarget)?;
158 let arguments = candid::encode_one(UploadCanisterSnapshotDataArgs {
159 canister_id: target,
160 snapshot_id: snapshot_id.to_vec(),
161 kind,
162 chunk: chunk.to_vec(),
163 })
164 .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
165 Self::from_arguments(
166 metadata.source_plan,
167 metadata.source,
168 &metadata.source_checksum,
169 IcSnapshotUploadKind::Data {
170 snapshot_id: snapshot_id.to_vec(),
171 source_kind,
172 chunk_checksum: checksum,
173 metadata_request: metadata.digest(),
174 },
175 arguments,
176 )
177 }
178
179 fn from_arguments(
180 source_plan: &'source OperationPlanRecord,
181 source: &'source IcSnapshotMetadataReply<'source>,
182 source_checksum: &ArtifactChecksumRecord,
183 kind: IcSnapshotUploadKind,
184 arguments: Vec<u8>,
185 ) -> Result<Self, IcSnapshotUploadError> {
186 if arguments.len() > MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES {
187 return Err(IcSnapshotUploadError::ArgumentsTooLarge);
188 }
189 let target = Principal::from_text(source.request().target())
190 .map_err(|_| IcRequestError::InvalidTarget)?;
191 Ok(Self {
192 source_plan,
193 source,
194 source_checksum: source_checksum.clone(),
195 kind,
196 arguments,
197 target_bytes: target.as_slice().to_vec(),
198 })
199 }
200
201 pub(crate) fn validate_data_destination(
202 &self,
203 snapshot_id: &[u8],
204 ) -> Result<(), IcSnapshotUploadError> {
205 if !matches!(self.kind, IcSnapshotUploadKind::Metadata) {
206 return Err(IcSnapshotUploadError::MetadataRequestRequired);
207 }
208 validate_destination(self.source, snapshot_id)
209 }
210
211 #[must_use]
213 pub const fn source_plan(&self) -> &'source OperationPlanRecord {
214 self.source_plan
215 }
216
217 #[must_use]
219 pub const fn source(&self) -> &'source IcSnapshotMetadataReply<'source> {
220 self.source
221 }
222 #[must_use]
224 pub const fn source_checksum(&self) -> &ArtifactChecksumRecord {
225 &self.source_checksum
226 }
227 #[must_use]
229 pub const fn kind(&self) -> &IcSnapshotUploadKind {
230 &self.kind
231 }
232 #[must_use]
234 pub fn target(&self) -> &str {
235 self.source.request().target()
236 }
237 #[must_use]
239 pub const fn receiver(&self) -> &'static str {
240 "aaaaa-aa"
241 }
242 #[must_use]
244 pub const fn method(&self) -> &'static str {
245 match self.kind {
246 IcSnapshotUploadKind::Metadata => "upload_canister_snapshot_metadata",
247 IcSnapshotUploadKind::Data { .. } => "upload_canister_snapshot_data",
248 }
249 }
250 #[must_use]
252 pub fn arguments(&self) -> &[u8] {
253 &self.arguments
254 }
255 #[must_use]
257 pub fn digest(&self) -> ArtifactChecksumRecord {
258 management_request_digest(&self.target_bytes, self.method(), &self.arguments)
259 }
260 #[must_use]
262 pub fn binding_digest(&self) -> ArtifactChecksumRecord {
263 let mut bytes = b"ic-backup/ic-snapshot-upload-binding/v1\0".to_vec();
264 bytes.extend_from_slice(self.source_plan.digest().hash().as_bytes());
265 bytes.extend_from_slice(self.source.digest().hash().as_bytes());
266 bytes.extend_from_slice(self.source_checksum.hash().as_bytes());
267 if let IcSnapshotUploadKind::Data {
268 metadata_request, ..
269 } = &self.kind
270 {
271 bytes.push(1);
272 bytes.extend_from_slice(metadata_request.hash().as_bytes());
273 } else {
274 bytes.push(0);
275 }
276 bytes.extend_from_slice(self.digest().hash().as_bytes());
277 ArtifactChecksumRecord::from_bytes(&bytes)
278 }
279}
280
281pub(super) fn validate_destination(
282 source: &IcSnapshotMetadataReply<'_>,
283 snapshot_id: &[u8],
284) -> Result<(), IcSnapshotUploadError> {
285 if snapshot_id.is_empty() || snapshot_id.len() > MAX_IC_SNAPSHOT_ID_BYTES {
286 return Err(IcSnapshotUploadError::InvalidDestination);
287 }
288 if snapshot_id == source.request().snapshot_id() {
289 return Err(IcSnapshotUploadError::DestinationReusesSource);
290 }
291 Ok(())
292}
293
294fn require_length(chunk: &[u8], length: u64) -> Result<(), IcSnapshotUploadError> {
295 if u64::try_from(chunk.len()).ok() != Some(length) {
296 return Err(IcSnapshotUploadError::ChunkLengthMismatch);
297 }
298 Ok(())
299}
300
301impl fmt::Debug for IcSnapshotUploadRequest<'_> {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 f.debug_struct("IcSnapshotUploadRequest")
304 .field("target", &self.target())
305 .field("method", &self.method())
306 .field("argument_bytes", &self.arguments.len())
307 .finish_non_exhaustive()
308 }
309}
310
311#[derive(Debug, Error)]
313pub enum IcSnapshotUploadError {
314 #[error("snapshot upload source target differs from the original selection")]
316 SourceTargetMismatch,
317 #[error("snapshot upload requires every original global value")]
319 UnavailableGlobal,
320 #[error("snapshot upload requires an original metadata request")]
322 MetadataRequestRequired,
323 #[error("snapshot upload destination ID is invalid")]
325 InvalidDestination,
326 #[error("snapshot upload destination reuses the original source ID")]
328 DestinationReusesSource,
329 #[error("snapshot upload chunk length differs from the source range")]
331 ChunkLengthMismatch,
332 #[error("snapshot upload chunk exceeds bound")]
334 ChunkTooLarge,
335 #[error("snapshot upload arguments exceed bound")]
337 ArgumentsTooLarge,
338 #[error("snapshot upload reply exceeds bound")]
340 ReplyTooLarge,
341 #[error("snapshot upload reply is invalid")]
343 InvalidReply,
344 #[error(transparent)]
346 Data(#[from] IcSnapshotDataError),
347 #[error(transparent)]
349 Checksum(#[from] ChecksumError),
350 #[error(transparent)]
352 Request(#[from] IcRequestError),
353}
354
355#[cfg(test)]
356mod tests;