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