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 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#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
67pub enum IntegrityCheckRequest {
68 Quick {
70 entity: IntegrityEntityIdentity,
72 },
73 DeepStart {
75 entity: IntegrityEntityIdentity,
77 submission_key: IntegritySubmissionKey,
79 },
80 DeepContinue {
82 job_id: IntegrityJobId,
84 acknowledged_sequence: u64,
86 },
87 DeepAbort {
89 job_id: IntegrityJobId,
91 },
92}
93
94impl IntegrityCheckRequest {
95 #[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 #[must_use]
106 pub const fn deep_abort(job_id: IntegrityJobId) -> Self {
107 Self::DeepAbort { job_id }
108 }
109}
110
111#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
114pub enum IntegrityCheckResult {
115 Quick(QuickIntegrityResult),
117 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#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
302pub struct DatabaseIncarnationId([u8; 16]);
303
304impl DatabaseIncarnationId {
305 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 #[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#[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 #[must_use]
403 pub const fn entity_tag(&self) -> u64 {
404 self.entity_tag
405 }
406
407 #[must_use]
409 pub const fn entity_path(&self) -> &str {
410 self.entity_path.as_str()
411 }
412
413 #[must_use]
415 pub const fn store_path(&self) -> &str {
416 self.store_path.as_str()
417 }
418}
419
420#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
422pub enum IntegrityAuthorityClass {
423 Corruption,
425 IncompatiblePersistedFormat,
427 InvariantViolation,
429 Unsupported,
431 Internal,
433}
434
435#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
437pub enum IntegrityFindingClass {
438 Corruption,
440 IncompatiblePersistedFormat,
442 ResourceLimited,
444}
445
446#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
449pub enum IntegrityFindingKind {
450 MalformedDataKey,
452
453 MalformedRow,
455
456 OversizedRow,
458
459 InvalidFieldValue,
461
462 PrimaryKeyMismatch,
464
465 InvalidIdentityValue,
467
468 IdentityHighWaterExceeded,
470
471 ConstraintViolation,
473
474 MissingIndexEntry,
476
477 DivergentIndexEntry,
479
480 MalformedIndexEntry,
482
483 OrphanIndexEntry,
485
486 DuplicateUniqueIndexKey,
488
489 MissingRelationTarget,
491
492 MissingReverseRelationEntry,
494
495 DivergentReverseRelationEntry,
497
498 MalformedReverseRelationEntry,
500
501 OrphanReverseRelationEntry,
503
504 MalformedJournalBatch,
506
507 JournalSequenceGap,
509
510 DuplicateJournalBatchIdentity,
512
513 JournalControlMismatch,
515}
516
517#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
520pub enum IntegrityPhase {
521 QuickMetadata,
523
524 Rows,
526
527 IndexEntries,
529
530 ReverseRelations,
532
533 JournalTails,
535
536 FinalProofVectorCheck,
538}
539
540#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
543pub enum IntegrityVerifierFamily {
544 DataKey,
546
547 RowEnvelope,
549
550 FieldValue,
552
553 PrimaryKey,
555
556 IdentityState,
558
559 ValidatedConstraints,
561
562 ForwardIndex,
564
565 IndexEntry,
567
568 UniqueIndex,
570
571 Relation,
573
574 ReverseRelationEntry,
576
577 JournalEnvelope,
579
580 JournalBatchIdentity,
582}
583
584#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
586pub enum IntegritySeverity {
587 Error,
589 Advisory,
591}
592
593#[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 #[must_use]
621 pub const fn diagnostic_code(&self) -> u16 {
622 self.diagnostic_code
623 }
624
625 #[must_use]
627 pub const fn class(&self) -> IntegrityFindingClass {
628 self.class
629 }
630
631 #[must_use]
633 pub const fn severity(&self) -> IntegritySeverity {
634 self.severity
635 }
636
637 #[must_use]
639 pub const fn kind(&self) -> IntegrityFindingKind {
640 self.kind
641 }
642
643 #[must_use]
645 pub const fn entity(&self) -> &IntegrityEntityIdentity {
646 &self.entity
647 }
648
649 #[must_use]
651 pub const fn store_path(&self) -> &str {
652 self.store_path.as_str()
653 }
654
655 #[must_use]
657 pub const fn phase(&self) -> IntegrityPhase {
658 self.phase
659 }
660
661 #[must_use]
663 pub const fn verifier_family(&self) -> IntegrityVerifierFamily {
664 self.verifier_family
665 }
666
667 #[must_use]
669 pub const fn physical_key(&self) -> &[u8] {
670 self.physical_key.as_slice()
671 }
672
673 #[must_use]
675 pub fn primary_key(&self) -> Option<&[u8]> {
676 self.primary_key.as_deref()
677 }
678
679 #[must_use]
681 pub const fn field_paths(&self) -> &[String] {
682 self.field_paths.as_slice()
683 }
684
685 #[must_use]
687 pub fn value_path(&self) -> Option<&ConstraintValuePath> {
688 self.value_path.as_deref()
689 }
690
691 #[must_use]
693 pub const fn constraint_id(&self) -> Option<u32> {
694 self.constraint_id
695 }
696
697 #[must_use]
699 pub fn constraint_name(&self) -> Option<&str> {
700 self.constraint_name.as_deref()
701 }
702
703 #[must_use]
705 pub const fn schema_index_id(&self) -> Option<u32> {
706 self.schema_index_id
707 }
708
709 #[must_use]
711 pub const fn relation_id(&self) -> Option<u32> {
712 self.relation_id
713 }
714
715 #[must_use]
717 pub fn expected(&self) -> Option<&str> {
718 self.expected.as_deref()
719 }
720
721 #[must_use]
723 pub fn observed(&self) -> Option<&str> {
724 self.observed.as_deref()
725 }
726}
727
728#[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 #[must_use]
756 pub const fn diagnostic_code(&self) -> u16 {
757 self.diagnostic_code
758 }
759
760 #[must_use]
762 pub const fn class(&self) -> IntegrityAuthorityClass {
763 self.class
764 }
765}
766
767#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
769pub struct IntegrityResourceDiagnostic {
770 diagnostic_code: u16,
771}
772
773impl IntegrityResourceDiagnostic {
774 #[must_use]
776 pub const fn diagnostic_code(&self) -> u16 {
777 self.diagnostic_code
778 }
779}
780
781#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
783pub enum QuickIntegrityStatus {
784 CompleteClean,
786 CompleteWithFindings,
788 Uninspectable(IntegrityAuthorityDiagnostic),
790 ResourceLimited(IntegrityResourceDiagnostic),
792}
793
794#[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 #[must_use]
810 pub const fn entity(&self) -> &IntegrityEntityIdentity {
811 &self.entity
812 }
813
814 #[must_use]
816 pub const fn database_incarnation_id(&self) -> DatabaseIncarnationId {
817 self.database_incarnation_id
818 }
819
820 #[must_use]
822 pub const fn accepted_schema_version(&self) -> u32 {
823 self.accepted_schema_version
824 }
825
826 #[must_use]
828 pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
829 self.accepted_schema_fingerprint
830 }
831
832 #[must_use]
834 pub const fn status(&self) -> &QuickIntegrityStatus {
835 &self.status
836 }
837
838 #[must_use]
840 pub const fn total_findings(&self) -> u64 {
841 self.total_findings
842 }
843
844 #[must_use]
846 pub const fn omitted_findings(&self) -> u64 {
847 self.omitted_findings
848 }
849
850 #[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
988pub(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}