Skip to main content

icydb_core/db/
integrity.rs

1//! Module: db::integrity
2//! Responsibility: bounded integrity-inspection result vocabulary and lifecycle identity.
3//! Does not own: accepted schema meaning, physical traversal, or inspection progress persistence.
4//! Boundary: database control + accepted inspection plan -> typed Quick inspection result.
5
6mod deep;
7mod derived;
8mod job;
9mod progress_codec;
10mod progress_store;
11mod proof;
12mod row;
13
14use crate::{
15    db::{
16        commit::{database_control_proof_identity, database_incarnation_id, ensure_recovered},
17        registry::{
18            StoreAllocationIdentities, StoreHandle, StoreRuntimeStorageCapabilities,
19            StoreRuntimeStorageMode,
20        },
21        schema::{
22            AcceptedInspectionPlan, IdentityStateLifecycle, MAX_IDENTITY_STATE_RECORDS_PER_DATABASE,
23        },
24    },
25    error::{ConstraintValuePath, ErrorClass, ErrorOrigin, InternalError},
26    traits::CanisterKind,
27};
28use candid::CandidType;
29use serde::Deserialize;
30use std::{
31    collections::BTreeMap,
32    sync::atomic::{AtomicU64, Ordering},
33};
34
35pub(in crate::db) use deep::{
36    abort_deep_integrity_job, continue_deep_integrity_job, run_next_integrity_retention_page,
37    start_deep_integrity_job,
38};
39pub(in crate::db) use derived::{
40    DerivedInspectionLimits, execute_index_integrity_page, execute_reverse_integrity_page,
41};
42pub use job::{
43    DeepIntegrityPage, DeepIntegrityPageStatus, IntegrityAbortReceipt, IntegrityAbortStatus,
44    IntegrityDeepError, IntegrityJobError, IntegrityJobId, IntegrityJobOwner, IntegrityJobReceipt,
45    IntegrityPendingTerminal, IntegritySubmissionKey, IntegrityTerminalOutcome,
46};
47pub(in crate::db) use job::{
48    IntegrityCheckpoint, IntegrityJob, IntegrityJobState, IntegrityReceiptEnvelope,
49    IntegrityReceiptReplayKey, MAX_INTEGRITY_IN_PROGRESS_PAGES,
50};
51pub(in crate::db) use progress_store::with_resumable_progress_store;
52pub(in crate::db) use proof::{IntegrityProofVector, capture_integrity_proof_vector};
53pub(in crate::db) use row::{
54    PhysicalUnitCheckpoint, RowInspectionLimits, execute_row_integrity_page,
55};
56
57pub(in crate::db) const MAX_INTEGRITY_PATH_BYTES: usize = 4 * 1024;
58
59/// One authorization-bound typed integrity operation.
60///
61/// Entity-bearing variants pin the generated selector identity that the
62/// session must match against current accepted authority. Continuation and
63/// abort carry only the opaque job identity; private checkpoints never cross
64/// this boundary.
65
66#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
67pub enum IntegrityCheckRequest {
68    /// Execute one bounded metadata/control inspection.
69    Quick {
70        /// Accepted entity selector to resolve and verify.
71        entity: IntegrityEntityIdentity,
72    },
73    /// Create or replay one idempotent Deep job.
74    DeepStart {
75        /// Accepted entity selector to resolve and verify.
76        entity: IntegrityEntityIdentity,
77        /// Owner-scoped idempotency key.
78        submission_key: IntegritySubmissionKey,
79    },
80    /// Advance or replay one retained Deep job.
81    DeepContinue {
82        /// Opaque engine-issued job identity.
83        job_id: IntegrityJobId,
84        /// Sequence of the outstanding receipt being acknowledged.
85        acknowledged_sequence: u64,
86    },
87    /// Freeze one retained Deep job for replayable abort.
88    DeepAbort {
89        /// Opaque engine-issued job identity.
90        job_id: IntegrityJobId,
91    },
92}
93
94impl IntegrityCheckRequest {
95    /// Build one Deep continuation or exact replay request.
96    #[must_use]
97    pub const fn deep_continue(job_id: IntegrityJobId, acknowledged_sequence: u64) -> Self {
98        Self::DeepContinue {
99            job_id,
100            acknowledged_sequence,
101        }
102    }
103
104    /// Build one replayable Deep-abort request.
105    #[must_use]
106    pub const fn deep_abort(job_id: IntegrityJobId) -> Self {
107        Self::DeepAbort { job_id }
108    }
109}
110
111/// Typed result shared by trusted Rust and SQL integrity frontends.
112
113#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
114pub enum IntegrityCheckResult {
115    /// Bounded one-call Quick result.
116    Quick(QuickIntegrityResult),
117    /// Start, continuation, terminal, or abort Deep receipt.
118    Deep(IntegrityJobReceipt),
119}
120
121fn validate_quick_integrity_control<C: CanisterKind>(
122    db: &crate::db::Db<C>,
123    plan: &AcceptedInspectionPlan,
124    incarnation: DatabaseIncarnationId,
125) -> Result<Vec<IntegrityFinding>, InternalError> {
126    let identity = plan.identity();
127    let source_store = db.store_handle(identity.store_path())?;
128    let relations = plan.relation_inspection();
129    let mut participating_stores =
130        BTreeMap::from([(identity.store_path().to_string(), source_store)]);
131    for relation in relations {
132        participating_stores
133            .entry(relation.target_store_path().to_string())
134            .or_insert_with(|| relation.target_store());
135    }
136
137    let _database_control = database_control_proof_identity()?;
138    proof::validate_integrity_allocation_registry()?;
139    validate_quick_identity_control(db, incarnation)?;
140    let mut findings = Vec::new();
141    for (store_path, store) in &participating_stores {
142        if let Some(finding) = validate_quick_store_control(plan, store_path, *store)? {
143            findings.push(finding);
144        }
145    }
146    for ordinal in 0..plan.index_inspection().len() {
147        let _domain = plan
148            .index_inspection()
149            .domain(ordinal, identity.entity_tag())?;
150    }
151
152    Ok(findings)
153}
154
155fn validate_quick_identity_control<C: CanisterKind>(
156    db: &crate::db::Db<C>,
157    incarnation: DatabaseIncarnationId,
158) -> Result<(), InternalError> {
159    let mut stores = db.with_store_registry(|registry| registry.iter().collect::<Vec<_>>());
160    stores.sort_unstable_by_key(|(store_path, _)| *store_path);
161
162    let mut owners = BTreeMap::new();
163    let mut state_count = 0usize;
164    for (store_path, store) in stores {
165        let states = store.with_schema(|schema_store| {
166            schema_store.identity_state_inventory_for_integrity(incarnation)
167        })?;
168        state_count = state_count
169            .checked_add(states.len())
170            .ok_or_else(InternalError::identity_state_corruption)?;
171        if state_count > MAX_IDENTITY_STATE_RECORDS_PER_DATABASE {
172            return Err(InternalError::identity_state_corruption());
173        }
174
175        for state in states {
176            let owner = state.owner();
177            record_quick_identity_owner(&mut owners, store_path, &state)?;
178            if state.lifecycle() == IdentityStateLifecycle::Active {
179                let runtime_entity = db
180                    .accepted_runtime_entity_for_tag(owner.entity_tag())
181                    .map_err(|_| InternalError::identity_state_corruption())?;
182                if runtime_entity.store_path() != store_path {
183                    return Err(InternalError::identity_state_corruption());
184                }
185            }
186        }
187    }
188
189    Ok(())
190}
191
192fn record_quick_identity_owner<'a>(
193    owners: &mut BTreeMap<(crate::types::EntityTag, crate::db::schema::FieldId), &'a str>,
194    store_path: &'a str,
195    state: &crate::db::schema::IdentityState,
196) -> Result<(), InternalError> {
197    let owner = state.owner();
198    let key = (owner.entity_tag(), owner.field_id());
199    if owners.insert(key, store_path).is_some() {
200        return Err(InternalError::identity_state_corruption());
201    }
202    Ok(())
203}
204
205fn validate_quick_store_control(
206    plan: &AcceptedInspectionPlan,
207    store_path: &str,
208    store: StoreHandle,
209) -> Result<Option<IntegrityFinding>, InternalError> {
210    let capabilities = store.storage_capabilities();
211    let allocations = store.allocation_identities();
212    match capabilities.storage_mode() {
213        StoreRuntimeStorageMode::Heap => {
214            if capabilities != StoreRuntimeStorageCapabilities::heap()
215                || allocations != StoreAllocationIdentities::absent()
216                || store.journal_tail_store().is_some()
217            {
218                return Err(InternalError::store_invariant());
219            }
220            Ok(None)
221        }
222        StoreRuntimeStorageMode::Journaled => {
223            if capabilities != StoreRuntimeStorageCapabilities::journaled()
224                || !allocations.matches_storage_capabilities(capabilities)
225            {
226                return Err(InternalError::store_invariant());
227            }
228            let journal = store
229                .journal_tail_store()
230                .ok_or_else(InternalError::store_invariant)?
231                .with_borrow(crate::db::journal::JournalTailStore::proof_identity)?;
232            if !journal.is_well_formed() {
233                return Ok(Some(quick_journal_control_finding(plan, store_path)));
234            }
235            Ok(None)
236        }
237    }
238}
239
240fn quick_journal_control_finding(
241    plan: &AcceptedInspectionPlan,
242    store_path: &str,
243) -> IntegrityFinding {
244    let error = InternalError::store_corruption();
245    IntegrityFinding {
246        diagnostic_code: error.diagnostic_code().error_code().raw(),
247        class: IntegrityFindingClass::Corruption,
248        severity: IntegritySeverity::Error,
249        kind: IntegrityFindingKind::JournalControlMismatch,
250        entity: IntegrityEntityIdentity::from_plan(plan),
251        store_path: store_path.to_string(),
252        phase: IntegrityPhase::QuickMetadata,
253        verifier_family: IntegrityVerifierFamily::JournalEnvelope,
254        physical_key: Vec::new(),
255        primary_key: None,
256        field_paths: Vec::new(),
257        value_path: None,
258        constraint_id: None,
259        constraint_name: None,
260        schema_index_id: None,
261        relation_id: None,
262        expected: Some("well-formed-journal-control".to_string()),
263        observed: Some("inconsistent-journal-control".to_string()),
264    }
265}
266
267fn relation_field_paths(plan: &AcceptedInspectionPlan, relation_id: u32) -> Vec<String> {
268    let snapshot = plan.snapshot().persisted_snapshot();
269    let Some(relation) = snapshot
270        .relations()
271        .iter()
272        .find(|relation| relation.id().get() == relation_id)
273    else {
274        return Vec::new();
275    };
276
277    relation
278        .local_field_ids()
279        .iter()
280        .filter_map(|field_id| {
281            snapshot
282                .fields()
283                .iter()
284                .find(|field| field.id() == *field_id)
285                .map(|field| field.name().to_string())
286        })
287        .collect()
288}
289
290const MAX_QUICK_RETURNED_FINDINGS: usize = 64;
291#[cfg(target_arch = "wasm32")]
292const DATABASE_INCARNATION_DOMAIN: &[u8] = b"icydb.database-incarnation.v1";
293static DATABASE_INCARNATION_SEQUENCE: AtomicU64 = AtomicU64::new(0);
294
295/// Durable identity of one database lifecycle.
296///
297/// The identity is independent of accepted schema, row, index, relation, and
298/// journal revisions. Ordinary reopen preserves it. Any future restore,
299/// replacement, or import lane that can reuse those revisions must mint and
300/// publish a fresh identity before the restored database becomes available.
301#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
302pub struct DatabaseIncarnationId([u8; 16]);
303
304impl DatabaseIncarnationId {
305    /// Decode one current-form nonzero incarnation identity.
306    pub(crate) fn try_from_bytes(bytes: [u8; 16]) -> Result<Self, InternalError> {
307        if bytes == [0; 16] {
308            return Err(InternalError::database_incarnation_invalid());
309        }
310
311        Ok(Self(bytes))
312    }
313
314    /// Return the canonical persisted identity bytes.
315    #[must_use]
316    pub const fn to_bytes(self) -> [u8; 16] {
317        self.0
318    }
319
320    fn generate() -> Result<Self, InternalError> {
321        let sequence = DATABASE_INCARNATION_SEQUENCE
322            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
323                current.checked_add(1)
324            })
325            .map_err(|_| InternalError::database_incarnation_generation_failed())?
326            .checked_add(1)
327            .ok_or_else(InternalError::database_incarnation_generation_failed)?;
328
329        #[cfg(not(target_arch = "wasm32"))]
330        let bytes = {
331            let mut bytes = [0_u8; 16];
332            getrandom::fill(&mut bytes)
333                .map_err(|_| InternalError::database_incarnation_generation_failed())?;
334            bytes
335        };
336
337        #[cfg(target_arch = "wasm32")]
338        let bytes = {
339            use sha2::{Digest, Sha256};
340
341            let mut hasher = Sha256::new();
342            hasher.update(DATABASE_INCARNATION_DOMAIN);
343            hasher.update(ic_cdk::api::canister_self().as_slice());
344            hasher.update(ic_cdk::api::time().to_be_bytes());
345            hasher.update(sequence.to_be_bytes());
346            let digest = hasher.finalize();
347            let mut bytes = [0_u8; 16];
348            bytes.copy_from_slice(&digest[..16]);
349            bytes
350        };
351
352        let _ = sequence;
353        Self::try_from_bytes(bytes)
354    }
355
356    #[cfg(test)]
357    pub(crate) const fn for_tests(fill: u8) -> Self {
358        let mut bytes = [fill; 16];
359        if fill == 0 {
360            bytes[15] = 1;
361        }
362        Self(bytes)
363    }
364}
365
366/// Stable entity identity projected into integrity responses.
367#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
368pub struct IntegrityEntityIdentity {
369    entity_tag: u64,
370    entity_path: String,
371    store_path: String,
372}
373
374impl IntegrityEntityIdentity {
375    fn from_plan(plan: &AcceptedInspectionPlan) -> Self {
376        Self::from_accepted_identity(plan.identity_ref())
377    }
378
379    pub(in crate::db) fn from_accepted_identity(
380        identity: &crate::db::schema::AcceptedCatalogIdentity,
381    ) -> Self {
382        Self {
383            entity_tag: identity.entity_tag().value(),
384            entity_path: identity.entity_path().to_string(),
385            store_path: identity.store_path().to_string(),
386        }
387    }
388
389    pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
390        if self.entity_tag == 0
391            || self.entity_path.is_empty()
392            || self.entity_path.len() > MAX_INTEGRITY_PATH_BYTES
393            || self.store_path.is_empty()
394            || self.store_path.len() > MAX_INTEGRITY_PATH_BYTES
395        {
396            return Err(IntegrityJobError::InvalidEntityIdentity);
397        }
398        Ok(())
399    }
400
401    /// Return the stable accepted entity tag.
402    #[must_use]
403    pub const fn entity_tag(&self) -> u64 {
404        self.entity_tag
405    }
406
407    /// Borrow the accepted entity path.
408    #[must_use]
409    pub const fn entity_path(&self) -> &str {
410        self.entity_path.as_str()
411    }
412
413    /// Borrow the accepted store path.
414    #[must_use]
415    pub const fn store_path(&self) -> &str {
416        self.store_path.as_str()
417    }
418}
419
420/// Broad machine-readable accepted-authority failure class.
421#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
422pub enum IntegrityAuthorityClass {
423    /// Accepted authority bytes or closure are corrupt.
424    Corruption,
425    /// Accepted authority uses an unsupported persisted form.
426    IncompatiblePersistedFormat,
427    /// Accepted authority violates an internal invariant.
428    InvariantViolation,
429    /// The selected entity or storage contract is unsupported.
430    Unsupported,
431    /// The engine could not complete accepted-authority inspection.
432    Internal,
433}
434
435/// Broad machine-readable integrity finding class.
436#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
437pub enum IntegrityFindingClass {
438    /// Accepted or physical bytes are corrupt.
439    Corruption,
440    /// Current-form persisted bytes cannot be decoded by this build.
441    IncompatiblePersistedFormat,
442    /// A required bounded proof could not be completed.
443    ResourceLimited,
444}
445
446/// Stable semantic family of one integrity finding.
447
448#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
449pub enum IntegrityFindingKind {
450    /// The physical data key is not a valid current key for its entity interval.
451    MalformedDataKey,
452
453    /// The maintained row envelope or slot table is malformed.
454    MalformedRow,
455
456    /// The row exceeds the maintained current raw-byte bound.
457    OversizedRow,
458
459    /// One active accepted field payload violates its exact field contract.
460    InvalidFieldValue,
461
462    /// The physical key and decoded primary-key field values disagree.
463    PrimaryKeyMismatch,
464
465    /// An Identity primary key is zero or has the wrong exact unsigned shape.
466    InvalidIdentityValue,
467
468    /// A live Identity primary key is above committed high-water.
469    IdentityHighWaterExceeded,
470
471    /// One validated accepted row-local constraint is violated.
472    ConstraintViolation,
473
474    /// One row-derived active forward-index witness is absent.
475    MissingIndexEntry,
476
477    /// One row-derived active forward-index witness has invalid value bytes.
478    DivergentIndexEntry,
479
480    /// One active forward-index entry has malformed key, identity, or value framing.
481    MalformedIndexEntry,
482
483    /// One active forward-index entry points at no authoritative source row.
484    OrphanIndexEntry,
485
486    /// One unique logical key has more than one physical row witness.
487    DuplicateUniqueIndexKey,
488
489    /// One accepted relation points to an absent target row.
490    MissingRelationTarget,
491
492    /// One expected active reverse-relation witness is absent.
493    MissingReverseRelationEntry,
494
495    /// One expected active reverse-relation witness has invalid value bytes.
496    DivergentReverseRelationEntry,
497
498    /// One active reverse-relation entry has malformed key, identity, or value framing.
499    MalformedReverseRelationEntry,
500
501    /// One active reverse-relation entry points at no authoritative source row.
502    OrphanReverseRelationEntry,
503
504    /// One durable journal batch is not a valid current-form envelope.
505    MalformedJournalBatch,
506
507    /// The durable journal tail omits one or more expected sequence values.
508    JournalSequenceGap,
509
510    /// Two durable journal batches carry the same logical batch identity.
511    DuplicateJournalBatchIdentity,
512
513    /// Bounded journal control records disagree without requiring tail traversal.
514    JournalControlMismatch,
515}
516
517/// Canonical Deep inspection phase.
518
519#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
520pub enum IntegrityPhase {
521    /// Bounded accepted metadata and control closure.
522    QuickMetadata,
523
524    /// Canonical physical row storage.
525    Rows,
526
527    /// Active forward-index storage.
528    IndexEntries,
529
530    /// Active source-owned reverse-relation storage.
531    ReverseRelations,
532
533    /// Durable journal tails.
534    JournalTails,
535
536    /// Final unchanged-proof-vector comparison.
537    FinalProofVectorCheck,
538}
539
540/// Deterministic verifier family within one physical inspection unit.
541
542#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
543pub enum IntegrityVerifierFamily {
544    /// Current physical data-key framing and identity.
545    DataKey,
546
547    /// Current row envelope, layout stamp, slot count, and table framing.
548    RowEnvelope,
549
550    /// One accepted field payload or frozen historical fill.
551    FieldValue,
552
553    /// Physical key versus accepted row primary-key fields.
554    PrimaryKey,
555
556    /// Accepted Identity owner and committed high-water.
557    IdentityState,
558
559    /// Validated accepted row-local constraints.
560    ValidatedConstraints,
561
562    /// One expected active forward-index witness.
563    ForwardIndex,
564
565    /// One physical active forward-index entry.
566    IndexEntry,
567
568    /// One unique-key multiplicity proof.
569    UniqueIndex,
570
571    /// One accepted relation's target and reverse witness projection.
572    Relation,
573
574    /// One physical active source-owned reverse-relation entry.
575    ReverseRelationEntry,
576
577    /// Current durable journal batch framing and sequence continuity.
578    JournalEnvelope,
579
580    /// Durable journal batch identity uniqueness.
581    JournalBatchIdentity,
582}
583
584/// Severity of one definite integrity finding.
585#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
586pub enum IntegritySeverity {
587    /// The finding identifies invalid maintained state.
588    Error,
589    /// The finding is an operator advisory and does not invalidate a clean proof.
590    Advisory,
591}
592
593/// One bounded machine-readable integrity finding.
594///
595/// Raw row payloads and unbounded application values are deliberately absent.
596#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
597pub struct IntegrityFinding {
598    diagnostic_code: u16,
599    class: IntegrityFindingClass,
600    severity: IntegritySeverity,
601    kind: IntegrityFindingKind,
602    entity: IntegrityEntityIdentity,
603    store_path: String,
604    phase: IntegrityPhase,
605    verifier_family: IntegrityVerifierFamily,
606    physical_key: Vec<u8>,
607    primary_key: Option<Vec<u8>>,
608    field_paths: Vec<String>,
609    value_path: Option<Box<ConstraintValuePath>>,
610    constraint_id: Option<u32>,
611    constraint_name: Option<String>,
612    schema_index_id: Option<u32>,
613    relation_id: Option<u32>,
614    expected: Option<String>,
615    observed: Option<String>,
616}
617
618impl IntegrityFinding {
619    /// Return the stable compact diagnostic code.
620    #[must_use]
621    pub const fn diagnostic_code(&self) -> u16 {
622        self.diagnostic_code
623    }
624
625    /// Return the broad finding class.
626    #[must_use]
627    pub const fn class(&self) -> IntegrityFindingClass {
628        self.class
629    }
630
631    /// Return the finding severity.
632    #[must_use]
633    pub const fn severity(&self) -> IntegritySeverity {
634        self.severity
635    }
636
637    /// Return the stable semantic finding family.
638    #[must_use]
639    pub const fn kind(&self) -> IntegrityFindingKind {
640        self.kind
641    }
642
643    /// Borrow the accepted entity identity.
644    #[must_use]
645    pub const fn entity(&self) -> &IntegrityEntityIdentity {
646        &self.entity
647    }
648
649    /// Borrow the affected store path.
650    #[must_use]
651    pub const fn store_path(&self) -> &str {
652        self.store_path.as_str()
653    }
654
655    /// Return the Deep phase that observed this finding.
656    #[must_use]
657    pub const fn phase(&self) -> IntegrityPhase {
658        self.phase
659    }
660
661    /// Return the deterministic verifier family that observed this finding.
662    #[must_use]
663    pub const fn verifier_family(&self) -> IntegrityVerifierFamily {
664        self.verifier_family
665    }
666
667    /// Borrow the bounded exact physical key.
668    #[must_use]
669    pub const fn physical_key(&self) -> &[u8] {
670        self.physical_key.as_slice()
671    }
672
673    /// Borrow the canonical primary-key suffix after successful key decoding.
674    #[must_use]
675    pub fn primary_key(&self) -> Option<&[u8]> {
676        self.primary_key.as_deref()
677    }
678
679    /// Borrow bounded accepted field paths relevant to the finding.
680    #[must_use]
681    pub const fn field_paths(&self) -> &[String] {
682        self.field_paths.as_slice()
683    }
684
685    /// Borrow the concrete accepted value path for targeted-rule findings.
686    #[must_use]
687    pub fn value_path(&self) -> Option<&ConstraintValuePath> {
688        self.value_path.as_deref()
689    }
690
691    /// Return the accepted constraint identity when applicable.
692    #[must_use]
693    pub const fn constraint_id(&self) -> Option<u32> {
694        self.constraint_id
695    }
696
697    /// Borrow the accepted constraint name when applicable.
698    #[must_use]
699    pub fn constraint_name(&self) -> Option<&str> {
700        self.constraint_name.as_deref()
701    }
702
703    /// Return the accepted logical index identity when applicable.
704    #[must_use]
705    pub const fn schema_index_id(&self) -> Option<u32> {
706        self.schema_index_id
707    }
708
709    /// Return the accepted relation identity when applicable.
710    #[must_use]
711    pub const fn relation_id(&self) -> Option<u32> {
712        self.relation_id
713    }
714
715    /// Borrow the bounded expected-state label, when applicable.
716    #[must_use]
717    pub fn expected(&self) -> Option<&str> {
718        self.expected.as_deref()
719    }
720
721    /// Borrow the bounded observed-state label, when applicable.
722    #[must_use]
723    pub fn observed(&self) -> Option<&str> {
724        self.observed.as_deref()
725    }
726}
727
728/// Typed reason that accepted authority could not be inspected.
729#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
730pub struct IntegrityAuthorityDiagnostic {
731    diagnostic_code: u16,
732    class: IntegrityAuthorityClass,
733}
734
735impl IntegrityAuthorityDiagnostic {
736    pub(in crate::db) fn from_internal(error: &InternalError) -> Self {
737        let class = match error.class {
738            ErrorClass::Corruption => IntegrityAuthorityClass::Corruption,
739            ErrorClass::IncompatiblePersistedFormat => {
740                IntegrityAuthorityClass::IncompatiblePersistedFormat
741            }
742            ErrorClass::InvariantViolation => IntegrityAuthorityClass::InvariantViolation,
743            ErrorClass::Unsupported | ErrorClass::NotFound | ErrorClass::Conflict => {
744                IntegrityAuthorityClass::Unsupported
745            }
746            ErrorClass::Internal => IntegrityAuthorityClass::Internal,
747        };
748        Self {
749            diagnostic_code: error.diagnostic_code().error_code().raw(),
750            class,
751        }
752    }
753
754    /// Return the stable compact diagnostic code.
755    #[must_use]
756    pub const fn diagnostic_code(&self) -> u16 {
757        self.diagnostic_code
758    }
759
760    /// Return the broad failure class.
761    #[must_use]
762    pub const fn class(&self) -> IntegrityAuthorityClass {
763        self.class
764    }
765}
766
767/// Typed bounded-resource failure.
768#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
769pub struct IntegrityResourceDiagnostic {
770    diagnostic_code: u16,
771}
772
773impl IntegrityResourceDiagnostic {
774    /// Return the stable compact diagnostic code.
775    #[must_use]
776    pub const fn diagnostic_code(&self) -> u16 {
777        self.diagnostic_code
778    }
779}
780
781/// Outcome of one bounded Quick integrity inspection.
782#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
783pub enum QuickIntegrityStatus {
784    /// Every bounded Quick family was inspected without findings.
785    CompleteClean,
786    /// Every bounded Quick family was inspected and definite findings exist.
787    CompleteWithFindings,
788    /// Load-bearing accepted authority could not be inspected.
789    Uninspectable(IntegrityAuthorityDiagnostic),
790    /// The minimum bounded inspection atom could not be completed.
791    ResourceLimited(IntegrityResourceDiagnostic),
792}
793
794/// Complete result of one bounded accepted-native Quick inspection.
795#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
796pub struct QuickIntegrityResult {
797    entity: IntegrityEntityIdentity,
798    database_incarnation_id: DatabaseIncarnationId,
799    accepted_schema_version: u32,
800    accepted_schema_fingerprint: [u8; 16],
801    status: QuickIntegrityStatus,
802    total_findings: u64,
803    omitted_findings: u64,
804    findings: Vec<IntegrityFinding>,
805}
806
807impl QuickIntegrityResult {
808    /// Borrow the accepted entity identity.
809    #[must_use]
810    pub const fn entity(&self) -> &IntegrityEntityIdentity {
811        &self.entity
812    }
813
814    /// Return the durable database incarnation inspected by this call.
815    #[must_use]
816    pub const fn database_incarnation_id(&self) -> DatabaseIncarnationId {
817        self.database_incarnation_id
818    }
819
820    /// Return the accepted entity schema version.
821    #[must_use]
822    pub const fn accepted_schema_version(&self) -> u32 {
823        self.accepted_schema_version
824    }
825
826    /// Return the accepted entity schema fingerprint.
827    #[must_use]
828    pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
829        self.accepted_schema_fingerprint
830    }
831
832    /// Borrow the Quick completion status.
833    #[must_use]
834    pub const fn status(&self) -> &QuickIntegrityStatus {
835        &self.status
836    }
837
838    /// Return the exact number of findings observed.
839    #[must_use]
840    pub const fn total_findings(&self) -> u64 {
841        self.total_findings
842    }
843
844    /// Return the number of findings omitted from the bounded response prefix.
845    #[must_use]
846    pub const fn omitted_findings(&self) -> u64 {
847        self.omitted_findings
848    }
849
850    /// Borrow the bounded canonical finding prefix.
851    #[must_use]
852    pub const fn findings(&self) -> &[IntegrityFinding] {
853        self.findings.as_slice()
854    }
855}
856
857struct QuickIntegrityAccumulator {
858    total_findings: u64,
859    findings: Vec<IntegrityFinding>,
860}
861
862impl QuickIntegrityAccumulator {
863    const fn new() -> Self {
864        Self {
865            total_findings: 0,
866            findings: Vec::new(),
867        }
868    }
869
870    fn record(&mut self, finding: IntegrityFinding) -> Result<(), IntegrityResourceDiagnostic> {
871        self.total_findings =
872            self.total_findings
873                .checked_add(1)
874                .ok_or(IntegrityResourceDiagnostic {
875                    diagnostic_code: icydb_diagnostic_code::ErrorCode::RUNTIME_INTERNAL.raw(),
876                })?;
877        if self.findings.len() < MAX_QUICK_RETURNED_FINDINGS {
878            self.findings.push(finding);
879        }
880        Ok(())
881    }
882
883    fn complete(
884        self,
885        plan: &AcceptedInspectionPlan,
886        incarnation: DatabaseIncarnationId,
887    ) -> Result<QuickIntegrityResult, InternalError> {
888        let status = if self.total_findings == 0 {
889            QuickIntegrityStatus::CompleteClean
890        } else {
891            QuickIntegrityStatus::CompleteWithFindings
892        };
893        let omitted_findings = self.omitted_findings()?;
894        let identity = plan.identity();
895
896        Ok(QuickIntegrityResult {
897            entity: IntegrityEntityIdentity::from_plan(plan),
898            database_incarnation_id: incarnation,
899            accepted_schema_version: identity.accepted_schema_version().get(),
900            accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
901            status,
902            total_findings: self.total_findings,
903            omitted_findings,
904            findings: self.findings,
905        })
906    }
907
908    fn resource_limited(
909        self,
910        plan: &AcceptedInspectionPlan,
911        incarnation: DatabaseIncarnationId,
912        diagnostic: IntegrityResourceDiagnostic,
913    ) -> Result<QuickIntegrityResult, InternalError> {
914        let omitted_findings = self.omitted_findings()?;
915        let identity = plan.identity();
916
917        Ok(QuickIntegrityResult {
918            entity: IntegrityEntityIdentity::from_plan(plan),
919            database_incarnation_id: incarnation,
920            accepted_schema_version: identity.accepted_schema_version().get(),
921            accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
922            status: QuickIntegrityStatus::ResourceLimited(diagnostic),
923            total_findings: self.total_findings,
924            omitted_findings,
925            findings: self.findings,
926        })
927    }
928
929    fn omitted_findings(&self) -> Result<u64, InternalError> {
930        let returned = u64::try_from(self.findings.len()).map_err(|_| {
931            InternalError::classified(ErrorClass::InvariantViolation, ErrorOrigin::Response)
932        })?;
933        self.total_findings.checked_sub(returned).ok_or_else(|| {
934            InternalError::classified(ErrorClass::InvariantViolation, ErrorOrigin::Response)
935        })
936    }
937}
938
939pub(in crate::db) fn uninspectable_quick_integrity(
940    identity: crate::db::schema::AcceptedCatalogIdentity,
941    incarnation: DatabaseIncarnationId,
942    error: &InternalError,
943) -> QuickIntegrityResult {
944    QuickIntegrityResult {
945        entity: IntegrityEntityIdentity::from_accepted_identity(&identity),
946        database_incarnation_id: incarnation,
947        accepted_schema_version: identity.accepted_schema_version().get(),
948        accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
949        status: QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic::from_internal(
950            error,
951        )),
952        total_findings: 0,
953        omitted_findings: 0,
954        findings: Vec::new(),
955    }
956}
957
958pub(in crate::db) fn execute_quick_integrity<C: CanisterKind>(
959    db: &crate::db::Db<C>,
960    plan: &AcceptedInspectionPlan,
961) -> Result<QuickIntegrityResult, InternalError> {
962    ensure_recovered(db)?;
963    let incarnation = database_incarnation_id()?;
964    let findings = match validate_quick_integrity_control(db, plan, incarnation) {
965        Ok(findings) => findings,
966        Err(error) => {
967            return Ok(uninspectable_quick_integrity(
968                plan.identity(),
969                incarnation,
970                &error,
971            ));
972        }
973    };
974    let mut accumulator = QuickIntegrityAccumulator::new();
975    for finding in findings {
976        if let Err(diagnostic) = accumulator.record(finding) {
977            return accumulator.resource_limited(plan, incarnation, diagnostic);
978        }
979    }
980
981    accumulator.complete(plan, incarnation)
982}
983
984pub(crate) fn generate_database_incarnation_id() -> Result<DatabaseIncarnationId, InternalError> {
985    DatabaseIncarnationId::generate()
986}
987
988/// Generate the database-lifecycle secret used to authenticate scalar cursors.
989///
990/// The key is persisted beside the database incarnation before runtime
991/// authority becomes visible. It uses the same process-local generation
992/// authority as generated ULID keys and is not exposed through an outward API.
993pub(crate) fn generate_cursor_authentication_key() -> Result<[u8; 32], InternalError> {
994    let first = <crate::types::Ulid as crate::types::GenerateKey>::generate()?;
995    let second = <crate::types::Ulid as crate::types::GenerateKey>::generate()?;
996    let mut bytes = [0_u8; 32];
997    bytes[..16].copy_from_slice(&first.to_bytes());
998    bytes[16..].copy_from_slice(&second.to_bytes());
999    if bytes == [0; 32] {
1000        return Err(InternalError::database_incarnation_generation_failed());
1001    }
1002
1003    Ok(bytes)
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008    use super::*;
1009    use crate::{
1010        db::schema::{FieldStorageDecode, LeafCodec, ScalarCodec},
1011        db::{
1012            commit::CommitSchemaFingerprint,
1013            schema::{
1014                AcceptedCatalogIdentity, AcceptedCompositeCatalog, AcceptedFieldKind,
1015                AcceptedSchemaRevision, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
1016                FieldId, IdentityState, IdentityStateOwner, PersistedFieldSnapshot,
1017                PersistedSchemaSnapshot, SchemaFieldSlot, SchemaInsertDefault, SchemaRowLayout,
1018                SchemaVersion, empty_accepted_enum_catalog_for_tests,
1019            },
1020        },
1021        types::EntityTag,
1022    };
1023
1024    fn plan() -> AcceptedInspectionPlan {
1025        let revision = AcceptedSchemaRevision::INITIAL;
1026        let identity = AcceptedCatalogIdentity::new(
1027            EntityTag::new(23),
1028            "tests::QuickEntity",
1029            "tests::QuickStore",
1030            revision,
1031            SchemaVersion::initial(),
1032            CommitSchemaFingerprint::from([0x44; 16]),
1033        );
1034        let snapshot = AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
1035            SchemaVersion::initial(),
1036            "tests::QuickEntity".to_string(),
1037            "QuickEntity".to_string(),
1038            FieldId::new(1),
1039            SchemaRowLayout::initial(vec![(FieldId::new(1), SchemaFieldSlot::new(0))]),
1040            vec![PersistedFieldSnapshot::new_initial(
1041                FieldId::new(1),
1042                "id".to_string(),
1043                SchemaFieldSlot::new(0),
1044                AcceptedFieldKind::Nat64,
1045                Vec::new(),
1046                false,
1047                SchemaInsertDefault::None,
1048                FieldStorageDecode::ByKind,
1049                LeafCodec::Scalar(ScalarCodec::Nat64),
1050            )],
1051        ));
1052        let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
1053            empty_accepted_enum_catalog_for_tests(),
1054            AcceptedCompositeCatalog::empty(),
1055            revision,
1056        );
1057
1058        AcceptedInspectionPlan::compile_relation_free_for_tests(identity, snapshot, value_catalog)
1059            .expect("accepted Quick plan should compile")
1060    }
1061
1062    fn finding(plan: &AcceptedInspectionPlan) -> IntegrityFinding {
1063        IntegrityFinding {
1064            diagnostic_code: icydb_diagnostic_code::ErrorCode::STORE_CORRUPTION.raw(),
1065            class: IntegrityFindingClass::Corruption,
1066            severity: IntegritySeverity::Error,
1067            kind: IntegrityFindingKind::MalformedRow,
1068            entity: IntegrityEntityIdentity::from_plan(plan),
1069            store_path: plan.identity().store_path().to_string(),
1070            phase: IntegrityPhase::Rows,
1071            verifier_family: IntegrityVerifierFamily::RowEnvelope,
1072            physical_key: vec![1],
1073            primary_key: None,
1074            field_paths: Vec::new(),
1075            value_path: None,
1076            constraint_id: None,
1077            constraint_name: None,
1078            schema_index_id: None,
1079            relation_id: None,
1080            expected: None,
1081            observed: None,
1082        }
1083    }
1084
1085    #[test]
1086    fn database_incarnation_rejects_zero_and_round_trips_current_bytes() {
1087        assert!(DatabaseIncarnationId::try_from_bytes([0; 16]).is_err());
1088
1089        let identity = DatabaseIncarnationId::for_tests(7);
1090        assert_eq!(
1091            DatabaseIncarnationId::try_from_bytes(identity.to_bytes())
1092                .expect("nonzero incarnation should decode"),
1093            identity,
1094        );
1095    }
1096
1097    #[test]
1098    fn integrity_finding_candid_preserves_targeted_constraint_path() {
1099        let plan = plan();
1100        let mut finding = finding(&plan);
1101        let path = ConstraintValuePath::new(vec![
1102            crate::error::ConstraintValuePathComponent::RootField { field_id: 1 },
1103            crate::error::ConstraintValuePathComponent::ListElement { index: 2 },
1104        ]);
1105        finding.kind = IntegrityFindingKind::ConstraintViolation;
1106        finding.value_path = Some(Box::new(path.clone()));
1107        finding.constraint_id = Some(7);
1108        finding.constraint_name = Some("nested_limit".to_string());
1109
1110        let bytes = candid::encode_one(&finding).expect("integrity finding should encode");
1111        let decoded: IntegrityFinding =
1112            candid::decode_one(&bytes).expect("integrity finding should decode");
1113        assert_eq!(decoded.value_path(), Some(&path));
1114        assert_eq!(decoded.constraint_id(), Some(7));
1115        assert_eq!(decoded.constraint_name(), Some("nested_limit"));
1116    }
1117
1118    #[test]
1119    fn quick_clean_result_binds_incarnation_and_accepted_plan_identity() {
1120        let plan = plan();
1121        let incarnation = DatabaseIncarnationId::for_tests(8);
1122        let result = QuickIntegrityAccumulator::new()
1123            .complete(&plan, incarnation)
1124            .expect("clean Quick accounting should remain valid");
1125
1126        assert_eq!(result.status(), &QuickIntegrityStatus::CompleteClean);
1127        assert_eq!(result.database_incarnation_id(), incarnation);
1128        assert_eq!(result.accepted_schema_version(), 1);
1129        assert_eq!(result.accepted_schema_fingerprint(), [0x44; 16]);
1130        assert_eq!(result.total_findings(), 0);
1131        assert_eq!(result.omitted_findings(), 0);
1132    }
1133
1134    #[test]
1135    fn quick_findings_keep_a_bounded_prefix_and_exact_omitted_count() {
1136        let plan = plan();
1137        let mut accumulator = QuickIntegrityAccumulator::new();
1138        for _ in 0..=MAX_QUICK_RETURNED_FINDINGS {
1139            accumulator
1140                .record(finding(&plan))
1141                .expect("bounded test finding count should fit");
1142        }
1143        let result = accumulator
1144            .complete(&plan, DatabaseIncarnationId::for_tests(9))
1145            .expect("one-over-cap Quick accounting should remain valid");
1146
1147        assert_eq!(result.status(), &QuickIntegrityStatus::CompleteWithFindings,);
1148        assert_eq!(result.total_findings(), 65);
1149        assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1150        assert_eq!(result.omitted_findings(), 1);
1151        assert_eq!(
1152            result.total_findings(),
1153            result.findings().len() as u64 + result.omitted_findings(),
1154        );
1155    }
1156
1157    #[test]
1158    fn quick_findings_at_the_exact_returned_cap_have_no_omissions() {
1159        let plan = plan();
1160        let mut accumulator = QuickIntegrityAccumulator::new();
1161        for _ in 0..MAX_QUICK_RETURNED_FINDINGS {
1162            accumulator
1163                .record(finding(&plan))
1164                .expect("exact-cap finding count should fit");
1165        }
1166        let result = accumulator
1167            .complete(&plan, DatabaseIncarnationId::for_tests(10))
1168            .expect("exact-cap Quick accounting should remain valid");
1169
1170        assert_eq!(result.total_findings(), 64);
1171        assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1172        assert_eq!(result.omitted_findings(), 0);
1173    }
1174
1175    #[test]
1176    fn quick_selected_authority_failure_is_not_a_clean_completion() {
1177        let plan = plan();
1178        let error = InternalError::accepted_row_constraint_program_corrupt();
1179        let result = uninspectable_quick_integrity(
1180            plan.identity(),
1181            DatabaseIncarnationId::for_tests(11),
1182            &error,
1183        );
1184
1185        assert!(matches!(
1186            result.status(),
1187            QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic {
1188                class: IntegrityAuthorityClass::Corruption,
1189                ..
1190            }),
1191        ));
1192        assert_eq!(result.total_findings(), 0);
1193        assert_eq!(result.omitted_findings(), 0);
1194    }
1195
1196    #[test]
1197    fn quick_identity_inventory_rejects_active_retired_owner_collision_first() {
1198        let incarnation = DatabaseIncarnationId::for_tests(12);
1199        let owner = IdentityStateOwner::try_new(incarnation, EntityTag::new(31), FieldId::new(1))
1200            .expect("identity owner should admit");
1201        let active = IdentityState::new_active(owner, AcceptedFieldKind::Nat64)
1202            .expect("active identity state should admit");
1203        let retired = active.retire().expect("active state should retire");
1204        let mut owners = BTreeMap::new();
1205
1206        record_quick_identity_owner(&mut owners, "tests::first", &active)
1207            .expect("the first owner should admit");
1208        let error = record_quick_identity_owner(&mut owners, "tests::second", &retired)
1209            .expect_err("an active/retired owner collision must reject");
1210
1211        assert_eq!(error.class(), ErrorClass::Corruption);
1212        assert_eq!(error.origin(), ErrorOrigin::Identity);
1213    }
1214}