Skip to main content

icydb_core/db/
read_set.rs

1//! Module: db::read_set
2//! Responsibility: bounded canonical source-revision proof vocabulary.
3//! Does not own: query planning, cursor traversal, or application job state.
4//! Boundary: registered physical stores + accepted runtime root -> public proof.
5
6use crate::{
7    db::{
8        QueryError,
9        codec::{finalize_hash_sha256, new_hash_sha256_prefixed, write_hash_str_u32},
10        integrity::DatabaseIncarnationId,
11        schema::AcceptedSchemaRuntimeRootIdentity,
12    },
13    error::InternalError,
14};
15use candid::CandidType;
16use serde::Deserialize;
17use std::{error::Error as StdError, fmt};
18
19const READ_SET_STORE_IDENTITY_DOMAIN: &[u8] = b"icydb.read-set.store.v1";
20const READ_SET_PROOF_FIXED_BYTES: usize = 16 + 8 + 1 + 32 + 4;
21const READ_SET_STORE_ENTRY_BYTES: usize = 32 + 8 + 8;
22
23/// Maximum physical stores admitted by one exhaustive source proof.
24pub const MAX_READ_SET_PROOF_STORES: usize = 64;
25const MAX_READ_SET_PROOF_STORES_U32: u32 = 64;
26/// Maximum canonical binary bytes admitted by one exhaustive source proof.
27pub const MAX_READ_SET_PROOF_BYTES: usize = 8 * 1024;
28const MAX_READ_SET_PROOF_BYTES_U32: u32 = 8 * 1024;
29
30/// Opaque canonical identity of one registered physical store.
31#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
32pub struct ReadSetStoreIdentity([u8; 32]);
33
34impl ReadSetStoreIdentity {
35    pub(in crate::db) fn for_store_path(store_path: &str) -> Self {
36        let mut hasher = new_hash_sha256_prefixed(READ_SET_STORE_IDENTITY_DOMAIN);
37        write_hash_str_u32(&mut hasher, store_path);
38        Self(finalize_hash_sha256(hasher))
39    }
40
41    pub(in crate::db) const fn from_bytes(bytes: [u8; 32]) -> Self {
42        Self(bytes)
43    }
44
45    /// Return the opaque canonical bytes.
46    #[must_use]
47    pub const fn to_bytes(self) -> [u8; 32] {
48        self.0
49    }
50}
51
52/// One physical store's row and access-state revisions.
53#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
54pub struct ReadSetStoreRevision {
55    store: ReadSetStoreIdentity,
56    data_revision: u64,
57    access_state_revision: u64,
58}
59
60impl ReadSetStoreRevision {
61    pub(in crate::db) const fn new(
62        store: ReadSetStoreIdentity,
63        data_revision: u64,
64        access_state_revision: u64,
65    ) -> Self {
66        Self {
67            store,
68            data_revision,
69            access_state_revision,
70        }
71    }
72
73    /// Return the physical store identity.
74    #[must_use]
75    pub const fn store(&self) -> ReadSetStoreIdentity {
76        self.store
77    }
78
79    /// Return the logical row-mutation revision.
80    #[must_use]
81    pub const fn data_revision(&self) -> u64 {
82        self.data_revision
83    }
84
85    /// Return the physical access-readiness revision.
86    #[must_use]
87    pub const fn access_state_revision(&self) -> u64 {
88        self.access_state_revision
89    }
90}
91
92/// Canonical bounded proof for every physical source store in one exhaustive job.
93#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
94pub struct ReadSetRevisionProof {
95    database_incarnation: [u8; 16],
96    accepted_root_revision: u64,
97    accepted_root_fingerprint_method: u8,
98    accepted_root_fingerprint: [u8; 32],
99    stores: Vec<ReadSetStoreRevision>,
100}
101
102impl ReadSetRevisionProof {
103    pub(in crate::db) fn new(
104        root: AcceptedSchemaRuntimeRootIdentity,
105        stores: Vec<ReadSetStoreRevision>,
106    ) -> Result<Self, ReadSetRevisionError> {
107        let (accepted_root_fingerprint_method, accepted_root_fingerprint) = root.fingerprint();
108        let proof = Self {
109            database_incarnation: root.database_incarnation().to_bytes(),
110            accepted_root_revision: root.accepted_root_revision().get(),
111            accepted_root_fingerprint_method,
112            accepted_root_fingerprint,
113            stores,
114        };
115        proof.validate()?;
116        Ok(proof)
117    }
118
119    pub(in crate::db) fn from_parts(
120        database_incarnation: [u8; 16],
121        accepted_root_revision: u64,
122        accepted_root_fingerprint_method: u8,
123        accepted_root_fingerprint: [u8; 32],
124        stores: Vec<ReadSetStoreRevision>,
125    ) -> Result<Self, ReadSetRevisionError> {
126        let proof = Self {
127            database_incarnation,
128            accepted_root_revision,
129            accepted_root_fingerprint_method,
130            accepted_root_fingerprint,
131            stores,
132        };
133        proof.validate()?;
134        Ok(proof)
135    }
136
137    /// Return the durable database lifecycle identity.
138    #[must_use]
139    pub const fn database_incarnation(&self) -> [u8; 16] {
140        self.database_incarnation
141    }
142
143    /// Return the accepted runtime-root revision.
144    #[must_use]
145    pub const fn accepted_root_revision(&self) -> u64 {
146        self.accepted_root_revision
147    }
148
149    /// Return the accepted runtime-root fingerprint method.
150    #[must_use]
151    pub const fn accepted_root_fingerprint_method(&self) -> u8 {
152        self.accepted_root_fingerprint_method
153    }
154
155    /// Return the accepted runtime-root fingerprint.
156    #[must_use]
157    pub const fn accepted_root_fingerprint(&self) -> [u8; 32] {
158        self.accepted_root_fingerprint
159    }
160
161    /// Borrow canonically sorted participating stores.
162    #[must_use]
163    pub const fn stores(&self) -> &[ReadSetStoreRevision] {
164        self.stores.as_slice()
165    }
166
167    /// Return the exact current canonical binary size.
168    #[must_use]
169    pub const fn encoded_len(&self) -> usize {
170        READ_SET_PROOF_FIXED_BYTES
171            .saturating_add(self.stores.len().saturating_mul(READ_SET_STORE_ENTRY_BYTES))
172    }
173
174    /// Validate bounds, nonzero revisions, and canonical store ordering.
175    pub fn validate(&self) -> Result<(), ReadSetRevisionError> {
176        if self.stores.is_empty() {
177            return Err(ReadSetRevisionError::Empty);
178        }
179        if self.stores.len() > MAX_READ_SET_PROOF_STORES {
180            return Err(ReadSetRevisionError::TooManyStores {
181                limit: MAX_READ_SET_PROOF_STORES_U32,
182                actual: u32::try_from(self.stores.len()).unwrap_or(u32::MAX),
183            });
184        }
185        let encoded_len = self.encoded_len();
186        if encoded_len > MAX_READ_SET_PROOF_BYTES {
187            return Err(ReadSetRevisionError::EncodedBytesExceeded {
188                limit: MAX_READ_SET_PROOF_BYTES_U32,
189                actual: u32::try_from(encoded_len).unwrap_or(u32::MAX),
190            });
191        }
192        if self.database_incarnation == [0; 16]
193            || self.accepted_root_revision == 0
194            || self.accepted_root_fingerprint_method == 0
195            || self.accepted_root_fingerprint == [0; 32]
196            || self.stores.iter().any(|store| {
197                store.store.to_bytes() == [0; 32]
198                    || store.data_revision == 0
199                    || store.access_state_revision == 0
200            })
201            || self
202                .stores
203                .windows(2)
204                .any(|pair| pair[0].store >= pair[1].store)
205        {
206            return Err(ReadSetRevisionError::NonCanonical);
207        }
208        Ok(())
209    }
210
211    pub(in crate::db) fn contains_store(&self, store: ReadSetStoreIdentity) -> bool {
212        self.stores
213            .binary_search_by_key(&store, ReadSetStoreRevision::store)
214            .is_ok()
215    }
216
217    pub(in crate::db) fn signature_bytes(&self) -> Vec<u8> {
218        let mut bytes = Vec::with_capacity(self.encoded_len());
219        bytes.extend_from_slice(&self.database_incarnation);
220        bytes.extend_from_slice(&self.accepted_root_revision.to_be_bytes());
221        bytes.push(self.accepted_root_fingerprint_method);
222        bytes.extend_from_slice(&self.accepted_root_fingerprint);
223        let store_count = u32::try_from(self.stores.len()).unwrap_or(u32::MAX);
224        bytes.extend_from_slice(&store_count.to_be_bytes());
225        for store in &self.stores {
226            bytes.extend_from_slice(&store.store.to_bytes());
227            bytes.extend_from_slice(&store.data_revision.to_be_bytes());
228            bytes.extend_from_slice(&store.access_state_revision.to_be_bytes());
229        }
230        bytes
231    }
232
233    pub(in crate::db) fn root_matches(
234        &self,
235        incarnation: DatabaseIncarnationId,
236        root: AcceptedSchemaRuntimeRootIdentity,
237    ) -> bool {
238        let (method, fingerprint) = root.fingerprint();
239        self.database_incarnation == incarnation.to_bytes()
240            && self.accepted_root_revision == root.accepted_root_revision().get()
241            && self.accepted_root_fingerprint_method == method
242            && self.accepted_root_fingerprint == fingerprint
243    }
244}
245
246/// Typed failure while capturing or validating an exhaustive source proof.
247#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
248pub enum ReadSetRevisionError {
249    /// No participating physical store was supplied.
250    Empty,
251    /// The proof exceeds the bounded participating-store count.
252    TooManyStores { limit: u32, actual: u32 },
253    /// The canonical proof encoding exceeds its byte ceiling.
254    EncodedBytesExceeded { limit: u32, actual: u32 },
255    /// Proof authority, revisions, or store ordering are not canonical.
256    NonCanonical,
257    /// One requested entity is absent from accepted runtime authority.
258    UnknownEntity,
259    /// The page's physical source was not declared in the initial proof.
260    StoreMissingFromProof { store: ReadSetStoreIdentity },
261    /// A continuation was supplied without its associated source proof.
262    ResumeProofRequired,
263    /// The database was recreated after the proof was captured.
264    DatabaseIncarnationChanged,
265    /// Accepted runtime authority changed after the proof was captured.
266    AcceptedRootChanged,
267    /// Rows in one participating physical store changed.
268    StoreDataChanged { store: ReadSetStoreIdentity },
269    /// Physical access readiness in one participating store changed.
270    StoreAccessChanged { store: ReadSetStoreIdentity },
271    /// A cross-call job attempted to use a volatile source store.
272    DurableStoreRequired { store: ReadSetStoreIdentity },
273}
274
275impl fmt::Display for ReadSetRevisionError {
276    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
277        formatter.write_str("exhaustive read source proof is invalid")
278    }
279}
280
281impl StdError for ReadSetRevisionError {}
282
283impl ReadSetRevisionError {
284    pub(in crate::db) const fn is_source_change(&self) -> bool {
285        matches!(
286            self,
287            Self::DatabaseIncarnationChanged
288                | Self::AcceptedRootChanged
289                | Self::StoreDataChanged { .. }
290                | Self::StoreAccessChanged { .. }
291        )
292    }
293}
294
295/// Query/runtime or typed source-proof failure from an exhaustive operation.
296#[derive(Debug)]
297pub enum ExhaustiveReadError {
298    /// Query planning, admission, execution, or database authority failed.
299    Query(QueryError),
300    /// The bounded source proof was invalid or changed.
301    Revision(ReadSetRevisionError),
302}
303
304impl fmt::Display for ExhaustiveReadError {
305    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
306        match self {
307            Self::Query(error) => error.fmt(formatter),
308            Self::Revision(error) => error.fmt(formatter),
309        }
310    }
311}
312
313impl StdError for ExhaustiveReadError {}
314
315impl From<QueryError> for ExhaustiveReadError {
316    fn from(error: QueryError) -> Self {
317        Self::Query(error)
318    }
319}
320
321impl From<InternalError> for ExhaustiveReadError {
322    fn from(error: InternalError) -> Self {
323        Self::Query(QueryError::execute(error))
324    }
325}
326
327impl From<ReadSetRevisionError> for ExhaustiveReadError {
328    fn from(error: ReadSetRevisionError) -> Self {
329        Self::Revision(error)
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    fn store(byte: u8, data_revision: u64) -> ReadSetStoreRevision {
338        ReadSetStoreRevision::new(
339            ReadSetStoreIdentity::from_bytes([byte; 32]),
340            data_revision,
341            1,
342        )
343    }
344
345    fn proof(
346        stores: Vec<ReadSetStoreRevision>,
347    ) -> Result<ReadSetRevisionProof, ReadSetRevisionError> {
348        ReadSetRevisionProof::from_parts([1; 16], 1, 1, [2; 32], stores)
349    }
350
351    #[test]
352    fn read_set_proof_requires_bounded_canonical_distinct_store_order() {
353        assert_eq!(proof(Vec::new()), Err(ReadSetRevisionError::Empty));
354        assert_eq!(
355            proof(vec![store(2, 1), store(1, 1)]),
356            Err(ReadSetRevisionError::NonCanonical),
357        );
358        assert_eq!(
359            proof(vec![store(1, 1), store(1, 2)]),
360            Err(ReadSetRevisionError::NonCanonical),
361        );
362
363        let too_many = (1..=MAX_READ_SET_PROOF_STORES + 1)
364            .map(|index| {
365                let mut identity = [0; 32];
366                identity[24..].copy_from_slice(
367                    &u64::try_from(index)
368                        .expect("bounded test store count should fit u64")
369                        .to_be_bytes(),
370                );
371                ReadSetStoreRevision::new(ReadSetStoreIdentity::from_bytes(identity), 1, 1)
372            })
373            .collect();
374        assert_eq!(
375            proof(too_many),
376            Err(ReadSetRevisionError::TooManyStores {
377                limit: MAX_READ_SET_PROOF_STORES_U32,
378                actual: u32::try_from(MAX_READ_SET_PROOF_STORES + 1)
379                    .expect("bounded test store count should fit u32"),
380            }),
381        );
382    }
383
384    #[test]
385    fn read_set_proof_rejects_zero_authority_or_revision_components() {
386        let valid = proof(vec![store(1, 1)]).expect("nonzero canonical proof should admit");
387        assert_eq!(
388            valid.encoded_len(),
389            READ_SET_PROOF_FIXED_BYTES + READ_SET_STORE_ENTRY_BYTES
390        );
391
392        assert_eq!(
393            ReadSetRevisionProof::from_parts([1; 16], 0, 1, [2; 32], vec![store(1, 1)]),
394            Err(ReadSetRevisionError::NonCanonical),
395        );
396        assert_eq!(
397            proof(vec![store(1, 0)]),
398            Err(ReadSetRevisionError::NonCanonical),
399        );
400    }
401}