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