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;