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