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