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