Skip to main content

ic_backup/model/ic_snapshot_coverage/
mod.rs

1//! Incremental coverage of admitted snapshot replies, without IO or durable progress.
2
3use super::{ic_snapshot_data::IcSnapshotDataReply, ic_snapshot_metadata::IcSnapshotMetadataReply};
4use ic_management_canister_types::SnapshotDataKind;
5use std::fmt;
6use thiserror::Error;
7
8/// Ephemeral coverage of exact retained metadata by already decoded data replies.
9///
10/// Each region advances contiguously from zero; regions may be interleaved and
11/// chunk-store replies may arrive in any order. Retained state is three nat64
12/// cursors and at most 1,024 chunk-presence bits, regardless of snapshot size.
13/// Admission retains no data bytes and performs no IO or provider calls. It does
14/// not establish authenticated origin, byte custody, durable storage, accounting
15/// or permission to repeat reads. Reconstructing this value starts empty; it is
16/// never a resume journal or a substitute for one.
17pub struct IcSnapshotDataCoverage<'metadata> {
18    metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
19    regions: [u64; 3],
20    chunks: Vec<bool>,
21}
22
23impl<'metadata> IcSnapshotDataCoverage<'metadata> {
24    /// Start empty coverage under one exact original metadata evidence owner.
25    #[must_use]
26    pub fn new(metadata: &'metadata IcSnapshotMetadataReply<'metadata>) -> Self {
27        Self {
28            metadata,
29            regions: [0; 3],
30            chunks: vec![false; metadata.metadata().wasm_chunk_store.len()],
31        }
32    }
33
34    /// Admit one validated reply, advancing only its region or exact chunk bit.
35    ///
36    /// Reply bytes may be dropped afterwards. That permits bounded streaming,
37    /// but coverage alone therefore makes no retained-byte or transfer attestation.
38    /// The order required within a region is a local admission order, not a grant
39    /// of remote dispatch order or another accounted read.
40    ///
41    /// # Errors
42    /// Rejects different metadata request/raw-reply evidence, gaps, overlaps,
43    /// duplicate ranges or duplicate chunk identities. Every error preserves
44    /// the previous coverage unchanged.
45    pub fn admit(
46        &mut self,
47        reply: &IcSnapshotDataReply<'_, '_>,
48    ) -> Result<(), IcSnapshotDataCoverageError> {
49        if reply.request().metadata().digest() != self.metadata.digest() {
50            return Err(IcSnapshotDataCoverageError::MetadataMismatch);
51        }
52        let (index, offset, size) = match reply.request().kind() {
53            SnapshotDataKind::WasmModule { offset, size } => (0, *offset, *size),
54            SnapshotDataKind::WasmMemory { offset, size } => (1, *offset, *size),
55            SnapshotDataKind::StableMemory { offset, size } => (2, *offset, *size),
56            SnapshotDataKind::WasmChunk { hash } => {
57                let index = self
58                    .metadata
59                    .metadata()
60                    .wasm_chunk_store
61                    .iter()
62                    .position(|chunk| chunk.hash == *hash)
63                    .ok_or(IcSnapshotDataCoverageError::MetadataMismatch)?;
64                if self.chunks[index] {
65                    return Err(IcSnapshotDataCoverageError::DuplicateChunk);
66                }
67                self.chunks[index] = true;
68                return Ok(());
69            }
70        };
71        if offset != self.regions[index] {
72            return Err(IcSnapshotDataCoverageError::NoncontiguousRange);
73        }
74        // Request admission already checked the exact metadata region and nat64
75        // addition. Keep this transition fallible without a second range policy.
76        let end = offset
77            .checked_add(size)
78            .ok_or(IcSnapshotDataCoverageError::NoncontiguousRange)?;
79        self.regions[index] = end;
80        Ok(())
81    }
82
83    /// Read exact original metadata, including non-data globals and optional fields.
84    #[must_use]
85    pub const fn metadata(&self) -> &'metadata IcSnapshotMetadataReply<'metadata> {
86        self.metadata
87    }
88
89    /// Read admitted module, heap and stable byte counts in that order.
90    #[must_use]
91    pub const fn covered_region_bytes(&self) -> [u64; 3] {
92        self.regions
93    }
94
95    /// Read the count of distinct admitted chunk-store replies, including empty chunks.
96    #[must_use]
97    pub fn covered_chunks(&self) -> usize {
98        self.chunks.iter().filter(|covered| **covered).count()
99    }
100
101    /// Project complete declared data coverage, without an effect or storage permit.
102    ///
103    /// Every region must reach its exact declared size and every known chunk must
104    /// have an admitted reply. Empty regions require no read; empty stored chunks
105    /// still require their hash-checked reply. There is no combined-size sum that
106    /// could overflow when independent regions use the full nat64 domain.
107    #[must_use]
108    pub fn complete(&self) -> Option<IcSnapshotDataCoverageView<'_, 'metadata>> {
109        let values = self.metadata.metadata();
110        let sizes = [
111            values.wasm_module_size,
112            values.wasm_memory_size,
113            values.stable_memory_size,
114        ];
115        (self.regions == sizes && self.chunks.iter().all(|covered| *covered))
116            .then_some(IcSnapshotDataCoverageView { coverage: self })
117    }
118}
119
120impl fmt::Debug for IcSnapshotDataCoverage<'_> {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        f.debug_struct("IcSnapshotDataCoverage")
123            .field("covered_region_bytes", &self.regions)
124            .field("covered_chunks", &self.covered_chunks())
125            .finish_non_exhaustive()
126    }
127}
128
129/// Read-only complete coverage of locally admitted replies, borrowing its owner.
130///
131/// This view authenticates no snapshot, retains no bytes and grants no durable
132/// transfer, journal completion, upload/load/start, terminal or release authority.
133#[derive(Debug)]
134pub struct IcSnapshotDataCoverageView<'coverage, 'metadata> {
135    coverage: &'coverage IcSnapshotDataCoverage<'metadata>,
136}
137
138impl<'metadata> IcSnapshotDataCoverageView<'_, 'metadata> {
139    /// Read the exact original metadata whose declared data has been covered.
140    #[must_use]
141    pub const fn metadata(&self) -> &'metadata IcSnapshotMetadataReply<'metadata> {
142        self.coverage.metadata()
143    }
144}
145
146/// Typed coverage rejection without payloads, identifiers or raw decoder details.
147#[derive(Debug, Error, Eq, PartialEq)]
148pub enum IcSnapshotDataCoverageError {
149    /// The request or raw metadata evidence differs from the original owner.
150    #[error("snapshot data coverage metadata mismatch")]
151    MetadataMismatch,
152    /// The next range leaves a gap, overlaps or duplicates admitted bytes.
153    #[error("snapshot data coverage requires the next contiguous range")]
154    NoncontiguousRange,
155    /// The exact stored chunk already has an admitted reply.
156    #[error("snapshot data coverage already contains this chunk")]
157    DuplicateChunk,
158}
159
160#[cfg(test)]
161mod tests;