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