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::Principal;
22use ic_platform::management_canister_types::{
23    SnapshotDataKind, SnapshotDataOffset, UploadCanisterSnapshotDataArgs,
24    UploadCanisterSnapshotMetadataArgs,
25};
26use std::fmt;
27use thiserror::Error;
28
29/// Maximum upload arguments, including finite Candid overhead around a 1 MiB chunk.
30pub const MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES: usize = 2 * 1024 * 1024;
31/// Maximum raw upload reply bytes before finite decoding.
32pub const MAX_IC_SNAPSHOT_UPLOAD_REPLY_BYTES: usize = 4096;
33
34/// Closed upload declaration; each branch retains exact original source association.
35#[derive(Debug)]
36pub enum IcSnapshotUploadKind {
37    /// Allocate a new snapshot with exact representable metadata, without replacement.
38    Metadata,
39    /// Write exact bounded bytes into an independently qualified new upload snapshot.
40    Data {
41        /// Exact destination raw ID, separate from the retained source raw ID/token.
42        snapshot_id: Vec<u8>,
43        /// Original source extent or known hash; chunk hashes are not part of upload wire.
44        source_kind: SnapshotDataKind,
45        /// Hash of the actual bounded chunk bytes encoded in the arguments.
46        chunk_checksum: ArtifactChecksumRecord,
47        /// Exact original metadata-allocation wire request digest.
48        metadata_request: ArtifactChecksumRecord,
49    },
50}
51
52/// Ephemeral exact wire payload with retained source evidence; no dispatch permission.
53///
54/// Pure construction admits declarations only. Guarded persistence operations supply
55/// fresh local byte binding. Integrations qualify authentic source/complete transfer,
56/// new destination attribution, original spending, current controller/custody and
57/// same-release application safety. No serialization, replacement, relocation,
58/// allowance or automatic outcome is introduced.
59pub 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    /// 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 = 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    /// Encode one exact source extent/chunk for a separately qualified new raw ID.
120    ///
121    /// Requires the original metadata upload declaration. Regions are nonempty and
122    /// bounded to 1 MiB; an empty known chunk requires its exact SHA-256. Actual
123    /// new snapshot attribution is not inferred from a supplied ID or decoded reply.
124    /// Data requests include their new ID before their own plan/reservation is retained;
125    /// metadata authority never supplies data-call spending or resets prior journals.
126    /// # Errors
127    /// Rejects a non-metadata original, invalid/reused ID, source range/hash or byte
128    /// mismatch, excessive data/arguments and encoding failure.
129    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    /// Read the exact original source plan; later upload intent must retain its context.
220    #[must_use]
221    pub const fn source_plan(&self) -> &'source OperationPlanRecord {
222        self.source_plan
223    }
224
225    /// Read original metadata/request evidence; it authenticates no source.
226    #[must_use]
227    pub const fn source(&self) -> &'source IcSnapshotMetadataReply<'source> {
228        self.source
229    }
230    /// Read declared retained tree checksum; pure construction verifies no files.
231    #[must_use]
232    pub const fn source_checksum(&self) -> &ArtifactChecksumRecord {
233        &self.source_checksum
234    }
235    /// Read the closed kind and exact source/destination association.
236    #[must_use]
237    pub const fn kind(&self) -> &IcSnapshotUploadKind {
238        &self.kind
239    }
240    /// Read the same canonical source/destination canister target.
241    #[must_use]
242    pub fn target(&self) -> &str {
243        self.source.request().target()
244    }
245    /// Read fixed management receiver, separate from effective routing.
246    #[must_use]
247    pub const fn receiver(&self) -> &'static str {
248        "aaaaa-aa"
249    }
250    /// Read the exact replicated host-update method; no query mode exists.
251    #[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    /// Read exact canonical encoded bytes, without signing or spending.
259    #[must_use]
260    pub fn arguments(&self) -> &[u8] {
261        &self.arguments
262    }
263    /// Hash wire payload only, before plan/authority derivation, using its canonical owner.
264    #[must_use]
265    pub fn digest(&self) -> ArtifactChecksumRecord {
266        management_request_digest(&self.target_bytes, self.method(), &self.arguments)
267    }
268    /// Bind original source evidence/tree and metadata origin separately from wire intent.
269    #[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/// Typed bounded upload denial; no error changes retained spending or source evidence.
320#[derive(Debug, Error)]
321pub enum IcSnapshotUploadError {
322    /// Exact original source target is outside its retained plan selection.
323    #[error("snapshot upload source target differs from the original selection")]
324    SourceTargetMismatch,
325    /// A missing original global cannot be represented by the upstream upload type.
326    #[error("snapshot upload requires every original global value")]
327    UnavailableGlobal,
328    /// Data upload must retain its exact original metadata-allocation declaration.
329    #[error("snapshot upload requires an original metadata request")]
330    MetadataRequestRequired,
331    /// Destination raw bytes violate the existing 1..=256 ID bounds.
332    #[error("snapshot upload destination ID is invalid")]
333    InvalidDestination,
334    /// The new upload declaration attempts to reuse its retained original source ID.
335    #[error("snapshot upload destination reuses the original source ID")]
336    DestinationReusesSource,
337    /// Actual bounded bytes differ from the requested source range length.
338    #[error("snapshot upload chunk length differs from the source range")]
339    ChunkLengthMismatch,
340    /// Actual chunk exceeds 1 MiB before cloning or encoding.
341    #[error("snapshot upload chunk exceeds bound")]
342    ChunkTooLarge,
343    /// Encoded arguments exceed the separate 2 MiB upload ceiling.
344    #[error("snapshot upload arguments exceed bound")]
345    ArgumentsTooLarge,
346    /// Raw reply exceeds 4 KiB before decoding.
347    #[error("snapshot upload reply exceeds bound")]
348    ReplyTooLarge,
349    /// Finite exact method-specific reply decoding rejected bytes.
350    #[error("snapshot upload reply is invalid")]
351    InvalidReply,
352    /// Existing range/known-hash owner rejected the source kind.
353    #[error(transparent)]
354    Data(#[from] IcSnapshotDataError),
355    /// The exact original chunk digest differs.
356    #[error(transparent)]
357    Checksum(#[from] ChecksumError),
358    /// Canonical principal or upstream Candid encoding failed.
359    #[error(transparent)]
360    Request(#[from] IcRequestError),
361}
362
363#[cfg(test)]
364mod tests;