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