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