Skip to main content

ic_backup/model/ic_snapshot_upload/
mod.rs

1//! Exact same-target snapshot upload codecs and passive original-reservation binding.
2
3use crate::model::evidence_digest::EvidenceDigest;
4use crate::model::ic_snapshot_data::DataExtent;
5use ic_platform::candid_codec as contract;
6mod attempt;
7mod plan;
8pub(crate) use plan::UploadCompletion;
9mod reply;
10pub(crate) use attempt::original_authority;
11pub use attempt::{IcSnapshotUploadAttempt, IcSnapshotUploadAttemptError};
12pub(crate) use plan::DataUploadPreparation;
13pub use plan::IcSnapshotDataUploadPlan;
14pub use reply::{IcSnapshotUploadReply, IcSnapshotUploadReplyKind};
15
16use super::{
17    artifacts::{ArtifactChecksumRecord, ChecksumError},
18    ic_request::{IcRequestError, admits_snapshot_id, management_request_digest},
19    ic_snapshot_data::{IcSnapshotDataError, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES, validate_kind},
20    ic_snapshot_metadata::IcSnapshotMetadataReply,
21    operation_plan::OperationPlanRecord,
22};
23use ic_platform::management_canister_types::{
24    SnapshotDataKind, SnapshotDataOffset, UploadCanisterSnapshotDataArgs,
25    UploadCanisterSnapshotMetadataArgs,
26};
27use std::fmt;
28use thiserror::Error;
29
30/// Maximum upload arguments, including finite Candid overhead around a 1 MiB chunk.
31pub const MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES: usize = 2 * 1024 * 1024;
32/// Maximum raw upload reply bytes before finite decoding.
33pub const MAX_IC_SNAPSHOT_UPLOAD_REPLY_BYTES: usize = 4096;
34
35/// Closed upload declaration; each branch retains exact original source association.
36#[derive(Debug)]
37pub enum IcSnapshotUploadKind {
38    /// Allocate a new snapshot with exact representable metadata, without replacement.
39    Metadata,
40    /// Write exact bounded bytes into an independently qualified new upload snapshot.
41    Data {
42        /// Exact destination raw ID, separate from the retained source raw ID/token.
43        snapshot_id: Vec<u8>,
44        /// Original source extent or known hash; chunk hashes are not part of upload wire.
45        source_kind: SnapshotDataKind,
46        /// Hash of the actual bounded chunk bytes encoded in the arguments.
47        chunk_checksum: ArtifactChecksumRecord,
48        /// Exact original metadata-allocation wire request digest.
49        metadata_request: ArtifactChecksumRecord,
50    },
51}
52
53/// Ephemeral exact wire payload with retained source evidence; no dispatch permission.
54///
55/// Pure construction admits declarations only. Guarded persistence operations supply
56/// fresh local byte binding. Integrations qualify authentic source/complete transfer,
57/// new destination attribution, original spending, current controller/custody and
58/// same-release application safety. No serialization, replacement, relocation,
59/// allowance or automatic outcome is introduced.
60pub struct IcSnapshotUploadRequest<'source> {
61    source_plan: &'source OperationPlanRecord,
62    source: &'source IcSnapshotMetadataReply<'source>,
63    source_checksum: ArtifactChecksumRecord,
64    kind: IcSnapshotUploadKind,
65    arguments: Vec<u8>,
66}
67
68impl<'source> IcSnapshotUploadRequest<'source> {
69    /// Encode exact uploadable original metadata for the same declared canister.
70    ///
71    /// Globals retain order and bits. Unavailable slots reject; absent timer/hook
72    /// values stay absent and do not default to inactive/ready states. Replacement
73    /// is always absent; this payload has no source-snapshot deletion lane.
74    /// Build these nonrecursive bytes before retaining their original operation plan.
75    /// # Errors
76    /// Rejects unavailable globals, encoding failure and excessive arguments.
77    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 = source.request().principal();
97        let arguments = contract::encode_one(
98            &UploadCanisterSnapshotMetadataArgs {
99                canister_id: target,
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            MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES,
110        )
111        .map_err(|error| match error {
112            contract::Error::Limit => IcSnapshotUploadError::ArgumentsTooLarge,
113            contract::Error::Schema => IcRequestError::Encoding(format!("{error:?}")).into(),
114        })?;
115        Self::from_arguments(
116            source_plan,
117            source,
118            source_checksum,
119            IcSnapshotUploadKind::Metadata,
120            arguments,
121        )
122    }
123
124    /// Encode one exact source extent/chunk for a separately qualified new raw ID.
125    ///
126    /// Requires the original metadata upload declaration. Regions are nonempty and
127    /// bounded to 1 MiB; an empty known chunk requires its exact SHA-256. Actual
128    /// new snapshot attribution is not inferred from a supplied ID or decoded reply.
129    /// Data requests include their new ID before their own plan/reservation is retained;
130    /// metadata authority never supplies data-call spending or resets prior journals.
131    /// # Errors
132    /// Rejects a non-metadata original, invalid/reused ID, source range/hash or byte
133    /// mismatch, excessive data/arguments and encoding failure.
134    pub fn data(
135        metadata: &Self,
136        snapshot_id: &[u8],
137        source_kind: SnapshotDataKind,
138        chunk: &[u8],
139    ) -> Result<Self, IcSnapshotUploadError> {
140        metadata.validate_data_destination(snapshot_id)?;
141        validate_kind(metadata.source, &source_kind)?;
142        if chunk.len() > MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES {
143            return Err(IcSnapshotUploadError::ChunkTooLarge);
144        }
145        let checksum = ArtifactChecksumRecord::from_bytes(chunk);
146        let kind = match DataExtent::from(&source_kind) {
147            DataExtent::Region {
148                region,
149                offset,
150                size,
151            } => {
152                require_length(chunk, size)?;
153                region.upload_offset(offset)
154            }
155            DataExtent::Chunk { hash } => {
156                let digest = ic_host_artifacts::artifact::Sha256Digest::from_bytes(
157                    hash.try_into()
158                        .map_err(|_| IcSnapshotDataError::InvalidChunkHash)?,
159                );
160                checksum.verify(&digest.to_string())?;
161                SnapshotDataOffset::WasmChunk
162            }
163        };
164        let target = metadata.source.request().principal();
165        let arguments = contract::encode_one(
166            &UploadCanisterSnapshotDataArgs {
167                canister_id: target,
168                snapshot_id: snapshot_id.to_vec(),
169                kind,
170                chunk: chunk.to_vec(),
171            },
172            MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES,
173        )
174        .map_err(|error| match error {
175            contract::Error::Limit => IcSnapshotUploadError::ArgumentsTooLarge,
176            contract::Error::Schema => IcRequestError::Encoding(format!("{error:?}")).into(),
177        })?;
178        Self::from_arguments(
179            metadata.source_plan,
180            metadata.source,
181            &metadata.source_checksum,
182            IcSnapshotUploadKind::Data {
183                snapshot_id: snapshot_id.to_vec(),
184                source_kind,
185                chunk_checksum: checksum,
186                metadata_request: metadata.digest(),
187            },
188            arguments,
189        )
190    }
191
192    fn from_arguments(
193        source_plan: &'source OperationPlanRecord,
194        source: &'source IcSnapshotMetadataReply<'source>,
195        source_checksum: &ArtifactChecksumRecord,
196        kind: IcSnapshotUploadKind,
197        arguments: Vec<u8>,
198    ) -> Result<Self, IcSnapshotUploadError> {
199        if arguments.len() > MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES {
200            return Err(IcSnapshotUploadError::ArgumentsTooLarge);
201        }
202        Ok(Self {
203            source_plan,
204            source,
205            source_checksum: source_checksum.clone(),
206            kind,
207            arguments,
208        })
209    }
210
211    pub(crate) fn validate_data_destination(
212        &self,
213        snapshot_id: &[u8],
214    ) -> Result<(), IcSnapshotUploadError> {
215        if !matches!(self.kind, IcSnapshotUploadKind::Metadata) {
216            return Err(IcSnapshotUploadError::MetadataRequestRequired);
217        }
218        validate_destination(self.source, snapshot_id)
219    }
220
221    /// Read the exact original source plan; later upload intent must retain its context.
222    #[must_use]
223    pub const fn source_plan(&self) -> &'source OperationPlanRecord {
224        self.source_plan
225    }
226
227    /// Read original metadata/request evidence; it authenticates no source.
228    #[must_use]
229    pub const fn source(&self) -> &'source IcSnapshotMetadataReply<'source> {
230        self.source
231    }
232    /// Read declared retained tree checksum; pure construction verifies no files.
233    #[must_use]
234    pub const fn source_checksum(&self) -> &ArtifactChecksumRecord {
235        &self.source_checksum
236    }
237    /// Read the closed kind and exact source/destination association.
238    #[must_use]
239    pub const fn kind(&self) -> &IcSnapshotUploadKind {
240        &self.kind
241    }
242    /// Read the same canonical source/destination canister target.
243    #[must_use]
244    pub fn target(&self) -> &str {
245        self.source.request().target()
246    }
247    /// Read fixed management receiver, separate from effective routing.
248    #[must_use]
249    pub const fn receiver(&self) -> &'static str {
250        "aaaaa-aa"
251    }
252    /// Read the exact replicated host-update method; no query mode exists.
253    #[must_use]
254    pub const fn method(&self) -> &'static str {
255        match self.kind {
256            IcSnapshotUploadKind::Metadata => "upload_canister_snapshot_metadata",
257            IcSnapshotUploadKind::Data { .. } => "upload_canister_snapshot_data",
258        }
259    }
260    /// Read exact canonical encoded bytes, without signing or spending.
261    #[must_use]
262    pub fn arguments(&self) -> &[u8] {
263        &self.arguments
264    }
265    /// Hash wire payload only, before plan/authority derivation, using its canonical owner.
266    #[must_use]
267    pub fn digest(&self) -> ArtifactChecksumRecord {
268        management_request_digest(
269            self.source.request().principal().as_slice(),
270            self.method(),
271            &self.arguments,
272        )
273    }
274    /// Bind original source evidence/tree and metadata origin separately from wire intent.
275    #[must_use]
276    pub fn binding_digest(&self) -> ArtifactChecksumRecord {
277        let mut bytes = EvidenceDigest::new(b"ic-backup/ic-snapshot-upload-binding/v1\0");
278        bytes.checksum(&self.source_plan.digest());
279        bytes.checksum(&self.source.digest());
280        bytes.checksum(&self.source_checksum);
281        if let IcSnapshotUploadKind::Data {
282            metadata_request, ..
283        } = &self.kind
284        {
285            bytes.byte(1);
286            bytes.checksum(metadata_request);
287        } else {
288            bytes.byte(0);
289        }
290        bytes.checksum(&self.digest());
291        bytes.finish()
292    }
293}
294
295pub(super) fn validate_destination(
296    source: &IcSnapshotMetadataReply<'_>,
297    snapshot_id: &[u8],
298) -> Result<(), IcSnapshotUploadError> {
299    if !admits_snapshot_id(snapshot_id) {
300        return Err(IcSnapshotUploadError::InvalidDestination);
301    }
302    if snapshot_id == source.request().snapshot_id() {
303        return Err(IcSnapshotUploadError::DestinationReusesSource);
304    }
305    Ok(())
306}
307
308fn require_length(chunk: &[u8], length: u64) -> Result<(), IcSnapshotUploadError> {
309    if u64::try_from(chunk.len()).ok() != Some(length) {
310        return Err(IcSnapshotUploadError::ChunkLengthMismatch);
311    }
312    Ok(())
313}
314
315impl fmt::Debug for IcSnapshotUploadRequest<'_> {
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        f.debug_struct("IcSnapshotUploadRequest")
318            .field("target", &self.target())
319            .field("method", &self.method())
320            .field("argument_bytes", &self.arguments.len())
321            .finish_non_exhaustive()
322    }
323}
324
325/// Typed bounded upload denial; no error changes retained spending or source evidence.
326#[derive(Debug, Error)]
327pub enum IcSnapshotUploadError {
328    /// Exact original source target is outside its retained plan selection.
329    #[error("snapshot upload source target differs from the original selection")]
330    SourceTargetMismatch,
331    /// A missing original global cannot be represented by the upstream upload type.
332    #[error("snapshot upload requires every original global value")]
333    UnavailableGlobal,
334    /// Data upload must retain its exact original metadata-allocation declaration.
335    #[error("snapshot upload requires an original metadata request")]
336    MetadataRequestRequired,
337    /// Destination raw bytes violate the existing 1..=256 ID bounds.
338    #[error("snapshot upload destination ID is invalid")]
339    InvalidDestination,
340    /// The new upload declaration attempts to reuse its retained original source ID.
341    #[error("snapshot upload destination reuses the original source ID")]
342    DestinationReusesSource,
343    /// Actual bounded bytes differ from the requested source range length.
344    #[error("snapshot upload chunk length differs from the source range")]
345    ChunkLengthMismatch,
346    /// Actual chunk exceeds 1 MiB before cloning or encoding.
347    #[error("snapshot upload chunk exceeds bound")]
348    ChunkTooLarge,
349    /// Encoded arguments exceed the separate 2 MiB upload ceiling.
350    #[error("snapshot upload arguments exceed bound")]
351    ArgumentsTooLarge,
352    /// Raw reply exceeds 4 KiB before decoding.
353    #[error("snapshot upload reply exceeds bound")]
354    ReplyTooLarge,
355    /// Finite exact method-specific reply decoding rejected bytes.
356    #[error("snapshot upload reply is invalid")]
357    InvalidReply,
358    /// Existing range/known-hash owner rejected the source kind.
359    #[error(transparent)]
360    Data(#[from] IcSnapshotDataError),
361    /// The exact original chunk digest differs.
362    #[error(transparent)]
363    Checksum(#[from] ChecksumError),
364    /// Canonical principal or upstream Candid encoding failed.
365    #[error(transparent)]
366    Request(#[from] IcRequestError),
367}
368
369#[cfg(test)]
370mod tests;