1mod 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 proof::{IntegrityProofVector, capture_integrity_proof_vector};
52pub(in crate::db) use row::{
53 PhysicalUnitCheckpoint, RowInspectionLimits, execute_row_integrity_page,
54};
55
56pub(in crate::db) const MAX_INTEGRITY_PATH_BYTES: usize = 4 * 1024;
57
58#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
66pub enum IntegrityCheckRequest {
67 Quick {
69 entity: IntegrityEntityIdentity,
71 },
72 DeepStart {
74 entity: IntegrityEntityIdentity,
76 submission_key: IntegritySubmissionKey,
78 },
79 DeepContinue {
81 job_id: IntegrityJobId,
83 acknowledged_sequence: u64,
85 },
86 DeepAbort {
88 job_id: IntegrityJobId,
90 },
91}
92
93impl IntegrityCheckRequest {
94 #[must_use]
96 pub const fn deep_continue(job_id: IntegrityJobId, acknowledged_sequence: u64) -> Self {
97 Self::DeepContinue {
98 job_id,
99 acknowledged_sequence,
100 }
101 }
102
103 #[must_use]
105 pub const fn deep_abort(job_id: IntegrityJobId) -> Self {
106 Self::DeepAbort { job_id }
107 }
108}
109
110#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
113pub enum IntegrityCheckResult {
114 Quick(QuickIntegrityResult),
116 Deep(IntegrityJobReceipt),
118}
119
120fn validate_quick_integrity_control<C: CanisterKind>(
121 db: &crate::db::Db<C>,
122 plan: &AcceptedInspectionPlan,
123 incarnation: DatabaseIncarnationId,
124) -> Result<Vec<IntegrityFinding>, InternalError> {
125 let identity = plan.identity();
126 let source_store = db.store_handle(identity.store_path())?;
127 let relations = plan.relation_inspection();
128 let mut participating_stores =
129 BTreeMap::from([(identity.store_path().to_string(), source_store)]);
130 for relation in relations {
131 participating_stores
132 .entry(relation.target_store_path().to_string())
133 .or_insert_with(|| relation.target_store());
134 }
135
136 let _database_control = database_control_proof_identity()?;
137 proof::validate_integrity_allocation_registry()?;
138 validate_quick_identity_control(db, incarnation)?;
139 let mut findings = Vec::new();
140 for (store_path, store) in &participating_stores {
141 if let Some(finding) = validate_quick_store_control(plan, store_path, *store)? {
142 findings.push(finding);
143 }
144 }
145 for ordinal in 0..plan.index_inspection().len() {
146 let _domain = plan
147 .index_inspection()
148 .domain(ordinal, identity.entity_tag())?;
149 }
150
151 Ok(findings)
152}
153
154fn validate_quick_identity_control<C: CanisterKind>(
155 db: &crate::db::Db<C>,
156 incarnation: DatabaseIncarnationId,
157) -> Result<(), InternalError> {
158 let mut stores = db.with_store_registry(|registry| registry.iter().collect::<Vec<_>>());
159 stores.sort_unstable_by_key(|(store_path, _)| *store_path);
160
161 let mut owners = BTreeMap::new();
162 let mut state_count = 0usize;
163 for (store_path, store) in stores {
164 let states = store.with_schema(|schema_store| {
165 schema_store.identity_state_inventory_for_integrity(incarnation)
166 })?;
167 state_count = state_count
168 .checked_add(states.len())
169 .ok_or_else(InternalError::identity_state_corruption)?;
170 if state_count > MAX_IDENTITY_STATE_RECORDS_PER_DATABASE {
171 return Err(InternalError::identity_state_corruption());
172 }
173
174 for state in states {
175 let owner = state.owner();
176 record_quick_identity_owner(&mut owners, store_path, &state)?;
177 if state.lifecycle() == IdentityStateLifecycle::Active {
178 let runtime_entity = db
179 .accepted_runtime_entity_for_tag(owner.entity_tag())
180 .map_err(|_| InternalError::identity_state_corruption())?;
181 if runtime_entity.store_path() != store_path {
182 return Err(InternalError::identity_state_corruption());
183 }
184 }
185 }
186 }
187
188 Ok(())
189}
190
191fn record_quick_identity_owner<'a>(
192 owners: &mut BTreeMap<(crate::types::EntityTag, crate::db::schema::FieldId), &'a str>,
193 store_path: &'a str,
194 state: &crate::db::schema::IdentityState,
195) -> Result<(), InternalError> {
196 let owner = state.owner();
197 let key = (owner.entity_tag(), owner.field_id());
198 if owners.insert(key, store_path).is_some() {
199 return Err(InternalError::identity_state_corruption());
200 }
201 Ok(())
202}
203
204fn validate_quick_store_control(
205 plan: &AcceptedInspectionPlan,
206 store_path: &str,
207 store: StoreHandle,
208) -> Result<Option<IntegrityFinding>, InternalError> {
209 let capabilities = store.storage_capabilities();
210 let allocations = store.allocation_identities();
211 match capabilities.storage_mode() {
212 StoreRuntimeStorageMode::Heap => {
213 if capabilities != StoreRuntimeStorageCapabilities::heap()
214 || allocations != StoreAllocationIdentities::absent()
215 || store.journal_tail_store().is_some()
216 {
217 return Err(InternalError::store_invariant());
218 }
219 Ok(None)
220 }
221 StoreRuntimeStorageMode::Journaled => {
222 if capabilities != StoreRuntimeStorageCapabilities::journaled()
223 || !allocations.matches_storage_capabilities(capabilities)
224 {
225 return Err(InternalError::store_invariant());
226 }
227 let journal = store
228 .journal_tail_store()
229 .ok_or_else(InternalError::store_invariant)?
230 .with_borrow(crate::db::journal::JournalTailStore::proof_identity)?;
231 if !journal.is_well_formed() {
232 return Ok(Some(quick_journal_control_finding(plan, store_path)));
233 }
234 Ok(None)
235 }
236 }
237}
238
239fn quick_journal_control_finding(
240 plan: &AcceptedInspectionPlan,
241 store_path: &str,
242) -> IntegrityFinding {
243 let error = InternalError::store_corruption();
244 IntegrityFinding {
245 diagnostic_code: error.diagnostic_code().error_code().raw(),
246 class: IntegrityFindingClass::Corruption,
247 severity: IntegritySeverity::Error,
248 kind: IntegrityFindingKind::JournalControlMismatch,
249 entity: IntegrityEntityIdentity::from_plan(plan),
250 store_path: store_path.to_string(),
251 phase: IntegrityPhase::QuickMetadata,
252 verifier_family: IntegrityVerifierFamily::JournalEnvelope,
253 physical_key: Vec::new(),
254 primary_key: None,
255 field_paths: Vec::new(),
256 value_path: None,
257 constraint_id: None,
258 constraint_name: None,
259 schema_index_id: None,
260 relation_id: None,
261 expected: Some("well-formed-journal-control".to_string()),
262 observed: Some("inconsistent-journal-control".to_string()),
263 }
264}
265
266fn relation_field_paths(plan: &AcceptedInspectionPlan, relation_id: u32) -> Vec<String> {
267 let snapshot = plan.snapshot().persisted_snapshot();
268 let Some(relation) = snapshot
269 .relations()
270 .iter()
271 .find(|relation| relation.id().get() == relation_id)
272 else {
273 return Vec::new();
274 };
275
276 relation
277 .local_field_ids()
278 .iter()
279 .filter_map(|field_id| {
280 snapshot
281 .fields()
282 .iter()
283 .find(|field| field.id() == *field_id)
284 .map(|field| field.name().to_string())
285 })
286 .collect()
287}
288
289const MAX_QUICK_RETURNED_FINDINGS: usize = 64;
290#[cfg(target_arch = "wasm32")]
291const DATABASE_INCARNATION_DOMAIN: &[u8] = b"icydb.database-incarnation.v1";
292static DATABASE_INCARNATION_SEQUENCE: AtomicU64 = AtomicU64::new(0);
293
294#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
301pub struct DatabaseIncarnationId([u8; 16]);
302
303impl DatabaseIncarnationId {
304 pub(crate) fn try_from_bytes(bytes: [u8; 16]) -> Result<Self, InternalError> {
306 if bytes == [0; 16] {
307 return Err(InternalError::database_incarnation_invalid());
308 }
309
310 Ok(Self(bytes))
311 }
312
313 #[must_use]
315 pub const fn to_bytes(self) -> [u8; 16] {
316 self.0
317 }
318
319 fn generate() -> Result<Self, InternalError> {
320 let sequence = DATABASE_INCARNATION_SEQUENCE
321 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
322 current.checked_add(1)
323 })
324 .map_err(|_| InternalError::database_incarnation_generation_failed())?
325 .checked_add(1)
326 .ok_or_else(InternalError::database_incarnation_generation_failed)?;
327
328 #[cfg(not(target_arch = "wasm32"))]
329 let bytes = {
330 let mut bytes = [0_u8; 16];
331 getrandom::fill(&mut bytes)
332 .map_err(|_| InternalError::database_incarnation_generation_failed())?;
333 bytes
334 };
335
336 #[cfg(target_arch = "wasm32")]
337 let bytes = {
338 use sha2::{Digest, Sha256};
339
340 let mut hasher = Sha256::new();
341 hasher.update(DATABASE_INCARNATION_DOMAIN);
342 hasher.update(ic_cdk::api::canister_self().as_slice());
343 hasher.update(ic_cdk::api::time().to_be_bytes());
344 hasher.update(sequence.to_be_bytes());
345 let digest = hasher.finalize();
346 let mut bytes = [0_u8; 16];
347 bytes.copy_from_slice(&digest[..16]);
348 bytes
349 };
350
351 let _ = sequence;
352 Self::try_from_bytes(bytes)
353 }
354
355 #[cfg(test)]
356 pub(crate) const fn for_tests(fill: u8) -> Self {
357 let mut bytes = [fill; 16];
358 if fill == 0 {
359 bytes[15] = 1;
360 }
361 Self(bytes)
362 }
363}
364
365#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
367pub struct IntegrityEntityIdentity {
368 entity_tag: u64,
369 entity_path: String,
370 store_path: String,
371}
372
373impl IntegrityEntityIdentity {
374 fn from_plan(plan: &AcceptedInspectionPlan) -> Self {
375 Self::from_accepted_identity(plan.identity_ref())
376 }
377
378 pub(in crate::db) fn from_accepted_identity(
379 identity: &crate::db::schema::AcceptedCatalogIdentity,
380 ) -> Self {
381 Self {
382 entity_tag: identity.entity_tag().value(),
383 entity_path: identity.entity_path().to_string(),
384 store_path: identity.store_path().to_string(),
385 }
386 }
387
388 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
389 if self.entity_tag == 0
390 || self.entity_path.is_empty()
391 || self.entity_path.len() > MAX_INTEGRITY_PATH_BYTES
392 || self.store_path.is_empty()
393 || self.store_path.len() > MAX_INTEGRITY_PATH_BYTES
394 {
395 return Err(IntegrityJobError::InvalidEntityIdentity);
396 }
397 Ok(())
398 }
399
400 #[must_use]
402 pub const fn entity_tag(&self) -> u64 {
403 self.entity_tag
404 }
405
406 #[must_use]
408 pub const fn entity_path(&self) -> &str {
409 self.entity_path.as_str()
410 }
411
412 #[must_use]
414 pub const fn store_path(&self) -> &str {
415 self.store_path.as_str()
416 }
417}
418
419#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
421pub enum IntegrityAuthorityClass {
422 Corruption,
424 IncompatiblePersistedFormat,
426 InvariantViolation,
428 Unsupported,
430 Internal,
432}
433
434#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
436pub enum IntegrityFindingClass {
437 Corruption,
439 IncompatiblePersistedFormat,
441 ResourceLimited,
443}
444
445#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
448pub enum IntegrityFindingKind {
449 MalformedDataKey,
451
452 MalformedRow,
454
455 OversizedRow,
457
458 InvalidFieldValue,
460
461 PrimaryKeyMismatch,
463
464 InvalidIdentityValue,
466
467 IdentityHighWaterExceeded,
469
470 ConstraintViolation,
472
473 MissingIndexEntry,
475
476 DivergentIndexEntry,
478
479 MalformedIndexEntry,
481
482 OrphanIndexEntry,
484
485 DuplicateUniqueIndexKey,
487
488 MissingRelationTarget,
490
491 MissingReverseRelationEntry,
493
494 DivergentReverseRelationEntry,
496
497 MalformedReverseRelationEntry,
499
500 OrphanReverseRelationEntry,
502
503 MalformedJournalBatch,
505
506 JournalSequenceGap,
508
509 DuplicateJournalBatchIdentity,
511
512 JournalControlMismatch,
514}
515
516#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
519pub enum IntegrityPhase {
520 QuickMetadata,
522
523 Rows,
525
526 IndexEntries,
528
529 ReverseRelations,
531
532 JournalTails,
534
535 FinalProofVectorCheck,
537}
538
539#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
542pub enum IntegrityVerifierFamily {
543 DataKey,
545
546 RowEnvelope,
548
549 FieldValue,
551
552 PrimaryKey,
554
555 IdentityState,
557
558 ValidatedConstraints,
560
561 ForwardIndex,
563
564 IndexEntry,
566
567 UniqueIndex,
569
570 Relation,
572
573 ReverseRelationEntry,
575
576 JournalEnvelope,
578
579 JournalBatchIdentity,
581}
582
583#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
585pub enum IntegritySeverity {
586 Error,
588 Advisory,
590}
591
592#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
596pub struct IntegrityFinding {
597 diagnostic_code: u16,
598 class: IntegrityFindingClass,
599 severity: IntegritySeverity,
600 kind: IntegrityFindingKind,
601 entity: IntegrityEntityIdentity,
602 store_path: String,
603 phase: IntegrityPhase,
604 verifier_family: IntegrityVerifierFamily,
605 physical_key: Vec<u8>,
606 primary_key: Option<Vec<u8>>,
607 field_paths: Vec<String>,
608 value_path: Option<Box<ConstraintValuePath>>,
609 constraint_id: Option<u32>,
610 constraint_name: Option<String>,
611 schema_index_id: Option<u32>,
612 relation_id: Option<u32>,
613 expected: Option<String>,
614 observed: Option<String>,
615}
616
617impl IntegrityFinding {
618 #[must_use]
620 pub const fn diagnostic_code(&self) -> u16 {
621 self.diagnostic_code
622 }
623
624 #[must_use]
626 pub const fn class(&self) -> IntegrityFindingClass {
627 self.class
628 }
629
630 #[must_use]
632 pub const fn severity(&self) -> IntegritySeverity {
633 self.severity
634 }
635
636 #[must_use]
638 pub const fn kind(&self) -> IntegrityFindingKind {
639 self.kind
640 }
641
642 #[must_use]
644 pub const fn entity(&self) -> &IntegrityEntityIdentity {
645 &self.entity
646 }
647
648 #[must_use]
650 pub const fn store_path(&self) -> &str {
651 self.store_path.as_str()
652 }
653
654 #[must_use]
656 pub const fn phase(&self) -> IntegrityPhase {
657 self.phase
658 }
659
660 #[must_use]
662 pub const fn verifier_family(&self) -> IntegrityVerifierFamily {
663 self.verifier_family
664 }
665
666 #[must_use]
668 pub const fn physical_key(&self) -> &[u8] {
669 self.physical_key.as_slice()
670 }
671
672 #[must_use]
674 pub fn primary_key(&self) -> Option<&[u8]> {
675 self.primary_key.as_deref()
676 }
677
678 #[must_use]
680 pub const fn field_paths(&self) -> &[String] {
681 self.field_paths.as_slice()
682 }
683
684 #[must_use]
686 pub fn value_path(&self) -> Option<&ConstraintValuePath> {
687 self.value_path.as_deref()
688 }
689
690 #[must_use]
692 pub const fn constraint_id(&self) -> Option<u32> {
693 self.constraint_id
694 }
695
696 #[must_use]
698 pub fn constraint_name(&self) -> Option<&str> {
699 self.constraint_name.as_deref()
700 }
701
702 #[must_use]
704 pub const fn schema_index_id(&self) -> Option<u32> {
705 self.schema_index_id
706 }
707
708 #[must_use]
710 pub const fn relation_id(&self) -> Option<u32> {
711 self.relation_id
712 }
713
714 #[must_use]
716 pub fn expected(&self) -> Option<&str> {
717 self.expected.as_deref()
718 }
719
720 #[must_use]
722 pub fn observed(&self) -> Option<&str> {
723 self.observed.as_deref()
724 }
725}
726
727#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
729pub struct IntegrityAuthorityDiagnostic {
730 diagnostic_code: u16,
731 class: IntegrityAuthorityClass,
732}
733
734impl IntegrityAuthorityDiagnostic {
735 pub(in crate::db) fn from_internal(error: &InternalError) -> Self {
736 let class = match error.class {
737 ErrorClass::Corruption => IntegrityAuthorityClass::Corruption,
738 ErrorClass::IncompatiblePersistedFormat => {
739 IntegrityAuthorityClass::IncompatiblePersistedFormat
740 }
741 ErrorClass::InvariantViolation => IntegrityAuthorityClass::InvariantViolation,
742 ErrorClass::Unsupported | ErrorClass::NotFound | ErrorClass::Conflict => {
743 IntegrityAuthorityClass::Unsupported
744 }
745 ErrorClass::Internal => IntegrityAuthorityClass::Internal,
746 };
747 Self {
748 diagnostic_code: error.diagnostic_code().error_code().raw(),
749 class,
750 }
751 }
752
753 #[must_use]
755 pub const fn diagnostic_code(&self) -> u16 {
756 self.diagnostic_code
757 }
758
759 #[must_use]
761 pub const fn class(&self) -> IntegrityAuthorityClass {
762 self.class
763 }
764}
765
766#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
768pub struct IntegrityResourceDiagnostic {
769 diagnostic_code: u16,
770}
771
772impl IntegrityResourceDiagnostic {
773 #[must_use]
775 pub const fn diagnostic_code(&self) -> u16 {
776 self.diagnostic_code
777 }
778}
779
780#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
782pub enum QuickIntegrityStatus {
783 CompleteClean,
785 CompleteWithFindings,
787 Uninspectable(IntegrityAuthorityDiagnostic),
789 ResourceLimited(IntegrityResourceDiagnostic),
791}
792
793#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
795pub struct QuickIntegrityResult {
796 entity: IntegrityEntityIdentity,
797 database_incarnation_id: DatabaseIncarnationId,
798 accepted_schema_version: u32,
799 accepted_schema_fingerprint: [u8; 16],
800 status: QuickIntegrityStatus,
801 total_findings: u64,
802 omitted_findings: u64,
803 findings: Vec<IntegrityFinding>,
804}
805
806impl QuickIntegrityResult {
807 #[must_use]
809 pub const fn entity(&self) -> &IntegrityEntityIdentity {
810 &self.entity
811 }
812
813 #[must_use]
815 pub const fn database_incarnation_id(&self) -> DatabaseIncarnationId {
816 self.database_incarnation_id
817 }
818
819 #[must_use]
821 pub const fn accepted_schema_version(&self) -> u32 {
822 self.accepted_schema_version
823 }
824
825 #[must_use]
827 pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
828 self.accepted_schema_fingerprint
829 }
830
831 #[must_use]
833 pub const fn status(&self) -> &QuickIntegrityStatus {
834 &self.status
835 }
836
837 #[must_use]
839 pub const fn total_findings(&self) -> u64 {
840 self.total_findings
841 }
842
843 #[must_use]
845 pub const fn omitted_findings(&self) -> u64 {
846 self.omitted_findings
847 }
848
849 #[must_use]
851 pub const fn findings(&self) -> &[IntegrityFinding] {
852 self.findings.as_slice()
853 }
854}
855
856struct QuickIntegrityAccumulator {
857 total_findings: u64,
858 findings: Vec<IntegrityFinding>,
859}
860
861impl QuickIntegrityAccumulator {
862 const fn new() -> Self {
863 Self {
864 total_findings: 0,
865 findings: Vec::new(),
866 }
867 }
868
869 fn record(&mut self, finding: IntegrityFinding) -> Result<(), IntegrityResourceDiagnostic> {
870 self.total_findings =
871 self.total_findings
872 .checked_add(1)
873 .ok_or(IntegrityResourceDiagnostic {
874 diagnostic_code: icydb_diagnostic_code::ErrorCode::RUNTIME_INTERNAL.raw(),
875 })?;
876 if self.findings.len() < MAX_QUICK_RETURNED_FINDINGS {
877 self.findings.push(finding);
878 }
879 Ok(())
880 }
881
882 fn complete(
883 self,
884 plan: &AcceptedInspectionPlan,
885 incarnation: DatabaseIncarnationId,
886 ) -> Result<QuickIntegrityResult, InternalError> {
887 let status = if self.total_findings == 0 {
888 QuickIntegrityStatus::CompleteClean
889 } else {
890 QuickIntegrityStatus::CompleteWithFindings
891 };
892 let omitted_findings = self.omitted_findings()?;
893 let identity = plan.identity();
894
895 Ok(QuickIntegrityResult {
896 entity: IntegrityEntityIdentity::from_plan(plan),
897 database_incarnation_id: incarnation,
898 accepted_schema_version: identity.accepted_schema_version().get(),
899 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
900 status,
901 total_findings: self.total_findings,
902 omitted_findings,
903 findings: self.findings,
904 })
905 }
906
907 fn resource_limited(
908 self,
909 plan: &AcceptedInspectionPlan,
910 incarnation: DatabaseIncarnationId,
911 diagnostic: IntegrityResourceDiagnostic,
912 ) -> Result<QuickIntegrityResult, InternalError> {
913 let omitted_findings = self.omitted_findings()?;
914 let identity = plan.identity();
915
916 Ok(QuickIntegrityResult {
917 entity: IntegrityEntityIdentity::from_plan(plan),
918 database_incarnation_id: incarnation,
919 accepted_schema_version: identity.accepted_schema_version().get(),
920 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
921 status: QuickIntegrityStatus::ResourceLimited(diagnostic),
922 total_findings: self.total_findings,
923 omitted_findings,
924 findings: self.findings,
925 })
926 }
927
928 fn omitted_findings(&self) -> Result<u64, InternalError> {
929 let returned = u64::try_from(self.findings.len()).map_err(|_| {
930 InternalError::classified(ErrorClass::InvariantViolation, ErrorOrigin::Response)
931 })?;
932 self.total_findings.checked_sub(returned).ok_or_else(|| {
933 InternalError::classified(ErrorClass::InvariantViolation, ErrorOrigin::Response)
934 })
935 }
936}
937
938pub(in crate::db) fn uninspectable_quick_integrity(
939 identity: crate::db::schema::AcceptedCatalogIdentity,
940 incarnation: DatabaseIncarnationId,
941 error: &InternalError,
942) -> QuickIntegrityResult {
943 QuickIntegrityResult {
944 entity: IntegrityEntityIdentity::from_accepted_identity(&identity),
945 database_incarnation_id: incarnation,
946 accepted_schema_version: identity.accepted_schema_version().get(),
947 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
948 status: QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic::from_internal(
949 error,
950 )),
951 total_findings: 0,
952 omitted_findings: 0,
953 findings: Vec::new(),
954 }
955}
956
957pub(in crate::db) fn execute_quick_integrity<C: CanisterKind>(
958 db: &crate::db::Db<C>,
959 plan: &AcceptedInspectionPlan,
960) -> Result<QuickIntegrityResult, InternalError> {
961 ensure_recovered(db)?;
962 let incarnation = database_incarnation_id()?;
963 let findings = match validate_quick_integrity_control(db, plan, incarnation) {
964 Ok(findings) => findings,
965 Err(error) => {
966 return Ok(uninspectable_quick_integrity(
967 plan.identity(),
968 incarnation,
969 &error,
970 ));
971 }
972 };
973 let mut accumulator = QuickIntegrityAccumulator::new();
974 for finding in findings {
975 if let Err(diagnostic) = accumulator.record(finding) {
976 return accumulator.resource_limited(plan, incarnation, diagnostic);
977 }
978 }
979
980 accumulator.complete(plan, incarnation)
981}
982
983pub(crate) fn generate_database_incarnation_id() -> Result<DatabaseIncarnationId, InternalError> {
984 DatabaseIncarnationId::generate()
985}
986
987pub(crate) fn generate_cursor_authentication_key() -> Result<[u8; 32], InternalError> {
993 let first = <crate::types::Ulid as crate::types::GenerateKey>::generate()?;
994 let second = <crate::types::Ulid as crate::types::GenerateKey>::generate()?;
995 let mut bytes = [0_u8; 32];
996 bytes[..16].copy_from_slice(&first.to_bytes());
997 bytes[16..].copy_from_slice(&second.to_bytes());
998 if bytes == [0; 32] {
999 return Err(InternalError::database_incarnation_generation_failed());
1000 }
1001
1002 Ok(bytes)
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008 use crate::{
1009 db::schema::{FieldStorageDecode, LeafCodec, ScalarCodec},
1010 db::{
1011 commit::CommitSchemaFingerprint,
1012 schema::{
1013 AcceptedCatalogIdentity, AcceptedCompositeCatalog, AcceptedFieldKind,
1014 AcceptedSchemaRevision, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
1015 FieldId, IdentityState, IdentityStateOwner, PersistedFieldSnapshot,
1016 PersistedSchemaSnapshot, SchemaFieldSlot, SchemaInsertDefault, SchemaRowLayout,
1017 SchemaVersion, empty_accepted_enum_catalog_for_tests,
1018 },
1019 },
1020 types::EntityTag,
1021 };
1022
1023 fn plan() -> AcceptedInspectionPlan {
1024 let revision = AcceptedSchemaRevision::INITIAL;
1025 let identity = AcceptedCatalogIdentity::new(
1026 EntityTag::new(23),
1027 "tests::QuickEntity",
1028 "tests::QuickStore",
1029 revision,
1030 SchemaVersion::initial(),
1031 CommitSchemaFingerprint::from([0x44; 16]),
1032 );
1033 let snapshot = AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
1034 SchemaVersion::initial(),
1035 "tests::QuickEntity".to_string(),
1036 "QuickEntity".to_string(),
1037 FieldId::new(1),
1038 SchemaRowLayout::initial(vec![(FieldId::new(1), SchemaFieldSlot::new(0))]),
1039 vec![PersistedFieldSnapshot::new_initial(
1040 FieldId::new(1),
1041 "id".to_string(),
1042 SchemaFieldSlot::new(0),
1043 AcceptedFieldKind::Nat64,
1044 Vec::new(),
1045 false,
1046 SchemaInsertDefault::None,
1047 FieldStorageDecode::ByKind,
1048 LeafCodec::Scalar(ScalarCodec::Nat64),
1049 )],
1050 ));
1051 let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
1052 empty_accepted_enum_catalog_for_tests(),
1053 AcceptedCompositeCatalog::empty(),
1054 revision,
1055 );
1056
1057 AcceptedInspectionPlan::compile_relation_free_for_tests(identity, snapshot, value_catalog)
1058 .expect("accepted Quick plan should compile")
1059 }
1060
1061 fn finding(plan: &AcceptedInspectionPlan) -> IntegrityFinding {
1062 IntegrityFinding {
1063 diagnostic_code: icydb_diagnostic_code::ErrorCode::STORE_CORRUPTION.raw(),
1064 class: IntegrityFindingClass::Corruption,
1065 severity: IntegritySeverity::Error,
1066 kind: IntegrityFindingKind::MalformedRow,
1067 entity: IntegrityEntityIdentity::from_plan(plan),
1068 store_path: plan.identity().store_path().to_string(),
1069 phase: IntegrityPhase::Rows,
1070 verifier_family: IntegrityVerifierFamily::RowEnvelope,
1071 physical_key: vec![1],
1072 primary_key: None,
1073 field_paths: Vec::new(),
1074 value_path: None,
1075 constraint_id: None,
1076 constraint_name: None,
1077 schema_index_id: None,
1078 relation_id: None,
1079 expected: None,
1080 observed: None,
1081 }
1082 }
1083
1084 #[test]
1085 fn database_incarnation_rejects_zero_and_round_trips_current_bytes() {
1086 assert!(DatabaseIncarnationId::try_from_bytes([0; 16]).is_err());
1087
1088 let identity = DatabaseIncarnationId::for_tests(7);
1089 assert_eq!(
1090 DatabaseIncarnationId::try_from_bytes(identity.to_bytes())
1091 .expect("nonzero incarnation should decode"),
1092 identity,
1093 );
1094 }
1095
1096 #[test]
1097 fn integrity_finding_candid_preserves_targeted_constraint_path() {
1098 let plan = plan();
1099 let mut finding = finding(&plan);
1100 let path = ConstraintValuePath::new(vec![
1101 crate::error::ConstraintValuePathComponent::RootField { field_id: 1 },
1102 crate::error::ConstraintValuePathComponent::ListElement { index: 2 },
1103 ]);
1104 finding.kind = IntegrityFindingKind::ConstraintViolation;
1105 finding.value_path = Some(Box::new(path.clone()));
1106 finding.constraint_id = Some(7);
1107 finding.constraint_name = Some("nested_limit".to_string());
1108
1109 let bytes = candid::encode_one(&finding).expect("integrity finding should encode");
1110 let decoded: IntegrityFinding =
1111 candid::decode_one(&bytes).expect("integrity finding should decode");
1112 assert_eq!(decoded.value_path(), Some(&path));
1113 assert_eq!(decoded.constraint_id(), Some(7));
1114 assert_eq!(decoded.constraint_name(), Some("nested_limit"));
1115 }
1116
1117 #[test]
1118 fn quick_clean_result_binds_incarnation_and_accepted_plan_identity() {
1119 let plan = plan();
1120 let incarnation = DatabaseIncarnationId::for_tests(8);
1121 let result = QuickIntegrityAccumulator::new()
1122 .complete(&plan, incarnation)
1123 .expect("clean Quick accounting should remain valid");
1124
1125 assert_eq!(result.status(), &QuickIntegrityStatus::CompleteClean);
1126 assert_eq!(result.database_incarnation_id(), incarnation);
1127 assert_eq!(result.accepted_schema_version(), 1);
1128 assert_eq!(result.accepted_schema_fingerprint(), [0x44; 16]);
1129 assert_eq!(result.total_findings(), 0);
1130 assert_eq!(result.omitted_findings(), 0);
1131 }
1132
1133 #[test]
1134 fn quick_findings_keep_a_bounded_prefix_and_exact_omitted_count() {
1135 let plan = plan();
1136 let mut accumulator = QuickIntegrityAccumulator::new();
1137 for _ in 0..=MAX_QUICK_RETURNED_FINDINGS {
1138 accumulator
1139 .record(finding(&plan))
1140 .expect("bounded test finding count should fit");
1141 }
1142 let result = accumulator
1143 .complete(&plan, DatabaseIncarnationId::for_tests(9))
1144 .expect("one-over-cap Quick accounting should remain valid");
1145
1146 assert_eq!(result.status(), &QuickIntegrityStatus::CompleteWithFindings,);
1147 assert_eq!(result.total_findings(), 65);
1148 assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1149 assert_eq!(result.omitted_findings(), 1);
1150 assert_eq!(
1151 result.total_findings(),
1152 result.findings().len() as u64 + result.omitted_findings(),
1153 );
1154 }
1155
1156 #[test]
1157 fn quick_findings_at_the_exact_returned_cap_have_no_omissions() {
1158 let plan = plan();
1159 let mut accumulator = QuickIntegrityAccumulator::new();
1160 for _ in 0..MAX_QUICK_RETURNED_FINDINGS {
1161 accumulator
1162 .record(finding(&plan))
1163 .expect("exact-cap finding count should fit");
1164 }
1165 let result = accumulator
1166 .complete(&plan, DatabaseIncarnationId::for_tests(10))
1167 .expect("exact-cap Quick accounting should remain valid");
1168
1169 assert_eq!(result.total_findings(), 64);
1170 assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1171 assert_eq!(result.omitted_findings(), 0);
1172 }
1173
1174 #[test]
1175 fn quick_selected_authority_failure_is_not_a_clean_completion() {
1176 let plan = plan();
1177 let error = InternalError::accepted_row_constraint_program_corrupt();
1178 let result = uninspectable_quick_integrity(
1179 plan.identity(),
1180 DatabaseIncarnationId::for_tests(11),
1181 &error,
1182 );
1183
1184 assert!(matches!(
1185 result.status(),
1186 QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic {
1187 class: IntegrityAuthorityClass::Corruption,
1188 ..
1189 }),
1190 ));
1191 assert_eq!(result.total_findings(), 0);
1192 assert_eq!(result.omitted_findings(), 0);
1193 }
1194
1195 #[test]
1196 fn quick_identity_inventory_rejects_active_retired_owner_collision_first() {
1197 let incarnation = DatabaseIncarnationId::for_tests(12);
1198 let owner = IdentityStateOwner::try_new(incarnation, EntityTag::new(31), FieldId::new(1))
1199 .expect("identity owner should admit");
1200 let active = IdentityState::new_active(owner, AcceptedFieldKind::Nat64)
1201 .expect("active identity state should admit");
1202 let retired = active.retire().expect("active state should retire");
1203 let mut owners = BTreeMap::new();
1204
1205 record_quick_identity_owner(&mut owners, "tests::first", &active)
1206 .expect("the first owner should admit");
1207 let error = record_quick_identity_owner(&mut owners, "tests::second", &retired)
1208 .expect_err("an active/retired owner collision must reject");
1209
1210 assert_eq!(error.class(), ErrorClass::Corruption);
1211 assert_eq!(error.origin(), ErrorOrigin::Identity);
1212 }
1213}