1use crate::db::{
7 codec::hex::decode_hex_nibble,
8 encode_hex_lower,
9 integrity::{
10 DatabaseIncarnationId, IntegrityAuthorityDiagnostic, IntegrityEntityIdentity,
11 IntegrityFinding, IntegrityFindingKind, IntegrityPhase, IntegrityProofVector,
12 IntegrityResourceDiagnostic, IntegrityVerifierFamily, MAX_INTEGRITY_PATH_BYTES,
13 PhysicalUnitCheckpoint,
14 },
15 journal::JournalInspectionCheckpoint,
16 schema::MAX_ACCEPTED_TARGET_PATH_COMPONENTS,
17};
18use crate::error::{ConstraintValuePath, ConstraintValuePathComponent, InternalError};
19
20use icydb_diagnostic_code::{DiagnosticDetail, RuntimeBoundaryCode};
21use serde::Deserialize;
22
23pub(in crate::db) const MAX_INTEGRITY_OWNER_BYTES: usize = 256;
24pub(in crate::db) const MAX_INTEGRITY_SUBMISSION_KEY_BYTES: usize = 256;
25pub(super) const MAX_INTEGRITY_RECEIPT_FINDINGS: usize = 64;
26pub(in crate::db) const MAX_INTEGRITY_IN_PROGRESS_PAGES: u64 = u64::MAX - 1;
27
28#[derive(
31 serde::Serialize, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd,
32)]
33#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
34pub struct IntegrityJobId([u8; 32]);
35
36impl IntegrityJobId {
37 pub fn try_from_bytes(bytes: [u8; 32]) -> Result<Self, IntegrityJobError> {
44 if bytes == [0; 32] {
45 return Err(IntegrityJobError::CorruptProgressRecord);
46 }
47 Ok(Self(bytes))
48 }
49
50 pub fn try_from_hex(value: &str) -> Result<Self, IntegrityJobError> {
57 if value.len() != 64 {
58 return Err(IntegrityJobError::InvalidJobId);
59 }
60
61 let mut bytes = [0_u8; 32];
62 for (index, pair) in value.as_bytes().as_chunks::<2>().0.iter().enumerate() {
63 let high = decode_hex_nibble(pair[0]).ok_or(IntegrityJobError::InvalidJobId)?;
64 let low = decode_hex_nibble(pair[1]).ok_or(IntegrityJobError::InvalidJobId)?;
65 bytes[index] = (high << 4) | low;
66 }
67 if bytes == [0; 32] {
68 return Err(IntegrityJobError::InvalidJobId);
69 }
70
71 Ok(Self(bytes))
72 }
73
74 #[must_use]
76 pub const fn to_bytes(self) -> [u8; 32] {
77 self.0
78 }
79
80 #[must_use]
82 pub fn to_hex(self) -> String {
83 encode_hex_lower(&self.0)
84 }
85
86 pub(in crate::db) fn validate(self) -> Result<(), IntegrityJobError> {
88 if self.0 == [0; 32] {
89 return Err(IntegrityJobError::InvalidJobId);
90 }
91 Ok(())
92 }
93}
94
95#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
98pub struct IntegrityJobOwner(String);
99
100impl IntegrityJobOwner {
101 pub fn new(value: impl Into<String>) -> Result<Self, IntegrityJobError> {
108 let value = value.into();
109 if value.is_empty() || value.len() > MAX_INTEGRITY_OWNER_BYTES {
110 return Err(IntegrityJobError::InvalidOwner);
111 }
112 Ok(Self(value))
113 }
114
115 #[must_use]
117 pub const fn as_str(&self) -> &str {
118 self.0.as_str()
119 }
120
121 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
123 if self.0.is_empty() || self.0.len() > MAX_INTEGRITY_OWNER_BYTES {
124 return Err(IntegrityJobError::InvalidOwner);
125 }
126 Ok(())
127 }
128}
129
130#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
133pub struct IntegritySubmissionKey(String);
134
135impl IntegritySubmissionKey {
136 pub fn new(value: impl Into<String>) -> Result<Self, IntegrityJobError> {
143 let value = value.into();
144 if value.is_empty() || value.len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES {
145 return Err(IntegrityJobError::InvalidSubmissionKey);
146 }
147 Ok(Self(value))
148 }
149
150 #[must_use]
152 pub const fn as_str(&self) -> &str {
153 self.0.as_str()
154 }
155
156 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
158 if self.0.is_empty() || self.0.len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES {
159 return Err(IntegrityJobError::InvalidSubmissionKey);
160 }
161 Ok(())
162 }
163}
164
165#[derive(Clone, Debug, Eq, PartialEq)]
167pub(in crate::db) enum IntegrityCheckpoint {
168 QuickMetadata,
170 Rows(PhysicalUnitCheckpoint),
172 Index {
174 ordinal: u32,
175 checkpoint: PhysicalUnitCheckpoint,
176 },
177 ReverseRelation {
179 ordinal: u32,
180 checkpoint: PhysicalUnitCheckpoint,
181 },
182 Journal {
184 store_ordinal: u32,
185 checkpoint: JournalInspectionCheckpoint,
186 },
187 FinalProof,
189}
190
191impl IntegrityCheckpoint {
192 #[must_use]
194 pub(in crate::db) const fn phase(&self) -> IntegrityPhase {
195 match self {
196 Self::QuickMetadata => IntegrityPhase::QuickMetadata,
197 Self::Rows(_) => IntegrityPhase::Rows,
198 Self::Index { .. } => IntegrityPhase::IndexEntries,
199 Self::ReverseRelation { .. } => IntegrityPhase::ReverseRelations,
200 Self::Journal { .. } => IntegrityPhase::JournalTails,
201 Self::FinalProof => IntegrityPhase::FinalProofVectorCheck,
202 }
203 }
204}
205
206#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
209#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
210pub enum IntegrityPendingTerminal {
211 Expired,
213 Aborted,
215}
216
217#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
220#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
221pub enum IntegrityTerminalOutcome {
222 DeepCompleteClean,
224 DeepCompleteWithFindings,
226 Invalidated,
228 Uninspectable(IntegrityAuthorityDiagnostic),
230 ResourceLimited(IntegrityResourceDiagnostic),
232 Expired,
234 Aborted,
236}
237
238impl IntegrityTerminalOutcome {
239 pub(in crate::db::integrity) fn from_internal(error: &InternalError) -> Self {
243 if matches!(
244 error.diagnostic().detail(),
245 Some(DiagnosticDetail::RuntimeBoundary {
246 boundary: RuntimeBoundaryCode::ExecutionBudgetExceeded
247 | RuntimeBoundaryCode::PageUnitTooLarge,
248 })
249 ) {
250 return Self::ResourceLimited(IntegrityResourceDiagnostic {
251 diagnostic_code: error.diagnostic_code().error_code().raw(),
252 });
253 }
254
255 Self::Uninspectable(IntegrityAuthorityDiagnostic::from_internal(error))
256 }
257}
258
259#[derive(Clone, Debug, Eq, PartialEq)]
261pub(in crate::db) enum IntegrityJobState {
262 InProgress,
264 TerminalPending(IntegrityPendingTerminal),
266 Terminal {
268 outcome: IntegrityTerminalOutcome,
269 receipt_acknowledged: bool,
270 },
271}
272
273#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
276#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
277pub enum DeepIntegrityPageStatus {
278 InProgress,
280 Terminal(IntegrityTerminalOutcome),
282}
283
284#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
287#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
288pub struct DeepIntegrityPage {
289 pub(super) job_id: IntegrityJobId,
290 pub(super) page_sequence: u64,
291 pub(super) phase: IntegrityPhase,
292 pub(super) status: DeepIntegrityPageStatus,
293 pub(super) pages_completed: u64,
294 pub(super) findings_seen: u64,
295 pub(super) findings: Vec<IntegrityFinding>,
296 pub(super) blocked_verifier_families: Vec<IntegrityVerifierFamily>,
297}
298
299impl DeepIntegrityPage {
300 #[must_use]
302 pub const fn job_id(&self) -> IntegrityJobId {
303 self.job_id
304 }
305
306 #[must_use]
308 pub const fn page_sequence(&self) -> u64 {
309 self.page_sequence
310 }
311
312 #[must_use]
314 pub const fn phase(&self) -> IntegrityPhase {
315 self.phase
316 }
317
318 #[must_use]
320 pub const fn status(&self) -> &DeepIntegrityPageStatus {
321 &self.status
322 }
323
324 #[must_use]
326 pub const fn pages_completed(&self) -> u64 {
327 self.pages_completed
328 }
329
330 #[must_use]
332 pub const fn findings_seen(&self) -> u64 {
333 self.findings_seen
334 }
335
336 #[must_use]
338 pub const fn findings(&self) -> &[IntegrityFinding] {
339 self.findings.as_slice()
340 }
341
342 #[must_use]
344 pub const fn blocked_verifier_families(&self) -> &[IntegrityVerifierFamily] {
345 self.blocked_verifier_families.as_slice()
346 }
347}
348
349#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
352#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
353pub enum IntegrityAbortStatus {
354 TerminationPending(IntegrityPendingTerminal),
356 Terminal(IntegrityTerminalOutcome),
358}
359
360#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
363#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
364pub struct IntegrityAbortReceipt {
365 pub(super) job_id: IntegrityJobId,
366 pub(super) page_sequence: u64,
367 pub(super) status: IntegrityAbortStatus,
368}
369
370impl IntegrityAbortReceipt {
371 #[must_use]
373 pub const fn job_id(&self) -> IntegrityJobId {
374 self.job_id
375 }
376
377 #[must_use]
379 pub const fn page_sequence(&self) -> u64 {
380 self.page_sequence
381 }
382
383 #[must_use]
385 pub const fn status(&self) -> &IntegrityAbortStatus {
386 &self.status
387 }
388}
389
390#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
393#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
394pub enum IntegrityJobReceipt {
395 Page(DeepIntegrityPage),
397 Abort(IntegrityAbortReceipt),
399}
400
401impl IntegrityJobReceipt {
402 #[must_use]
404 pub const fn job_id(&self) -> IntegrityJobId {
405 match self {
406 Self::Page(page) => page.job_id,
407 Self::Abort(receipt) => receipt.job_id,
408 }
409 }
410
411 #[must_use]
413 pub const fn page_sequence(&self) -> u64 {
414 match self {
415 Self::Page(page) => page.page_sequence,
416 Self::Abort(receipt) => receipt.page_sequence,
417 }
418 }
419}
420
421#[derive(Clone, Copy, Debug, Eq, PartialEq)]
423pub(in crate::db) enum IntegrityReceiptReplayKey {
424 Start,
426 Continue { acknowledged_sequence: u64 },
428}
429
430#[derive(Clone, Debug, Eq, PartialEq)]
432pub(in crate::db) struct IntegrityReceiptEnvelope {
433 pub(super) replay_key: IntegrityReceiptReplayKey,
434 pub(super) receipt: IntegrityJobReceipt,
435}
436
437#[derive(Clone, Debug, Eq, PartialEq)]
439pub(in crate::db) struct IntegrityJob {
440 pub(super) id: IntegrityJobId,
441 pub(super) database_incarnation_id: DatabaseIncarnationId,
442 pub(super) owner: IntegrityJobOwner,
443 pub(super) submission_key: IntegritySubmissionKey,
444 pub(super) entity: IntegrityEntityIdentity,
445 pub(super) accepted_schema_version: u32,
446 pub(super) accepted_schema_fingerprint: [u8; 16],
447 pub(super) inspection_plan_fingerprint: [u8; 32],
448 pub(super) checkpoint: IntegrityCheckpoint,
449 pub(super) captured_proof_vector: IntegrityProofVector,
450 pub(super) state: IntegrityJobState,
451 pub(super) lease_deadline_nanos: u64,
452 pub(super) findings_seen: u64,
453 pub(super) pages_completed: u64,
454 pub(super) blocked_verifier_families: Vec<IntegrityVerifierFamily>,
455 pub(super) last_receipt: IntegrityReceiptEnvelope,
456}
457
458impl IntegrityJob {
459 pub(super) fn validate(&self) -> Result<(), IntegrityJobError> {
461 if self.id != self.last_receipt.receipt.job_id()
462 || self.database_incarnation_id != self.captured_proof_vector.database_incarnation_id()
463 || self.accepted_schema_version != self.captured_proof_vector.accepted_schema_version()
464 || self.accepted_schema_fingerprint
465 != self.captured_proof_vector.accepted_schema_fingerprint()
466 || self.inspection_plan_fingerprint
467 != self.captured_proof_vector.inspection_plan_fingerprint()
468 || self.entity.entity_path().is_empty()
469 || self.entity.entity_path().len() > MAX_INTEGRITY_PATH_BYTES
470 || self.entity.store_path().is_empty()
471 || self.entity.store_path().len() > MAX_INTEGRITY_PATH_BYTES
472 || self.entity.entity_tag() == 0
473 || self.owner.as_str().is_empty()
474 || self.owner.as_str().len() > MAX_INTEGRITY_OWNER_BYTES
475 || self.submission_key.as_str().is_empty()
476 || self.submission_key.as_str().len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES
477 || self.accepted_schema_version == 0
478 || self.lease_deadline_nanos == 0
479 || matches!(
480 self.state,
481 IntegrityJobState::InProgress | IntegrityJobState::TerminalPending(_)
482 ) && self.pages_completed > MAX_INTEGRITY_IN_PROGRESS_PAGES
483 || !strictly_sorted_unique(&self.blocked_verifier_families)
484 || self.captured_proof_vector.validate().is_err()
485 || !self.checkpoint_is_well_formed()
486 {
487 return Err(IntegrityJobError::CorruptProgressRecord);
488 }
489
490 let receipt_matches_state = match (&self.state, &self.last_receipt.receipt) {
491 (
492 IntegrityJobState::InProgress | IntegrityJobState::TerminalPending(_),
493 IntegrityJobReceipt::Page(page),
494 ) => {
495 page.status == DeepIntegrityPageStatus::InProgress
496 && page.phase == self.checkpoint.phase()
497 && self.page_matches_counters(page)
498 }
499 (IntegrityJobState::Terminal { outcome, .. }, IntegrityJobReceipt::Page(page)) => {
500 page.status == DeepIntegrityPageStatus::Terminal(outcome.clone())
501 && page.phase == self.checkpoint.phase()
502 && !matches!(
503 outcome,
504 IntegrityTerminalOutcome::Expired | IntegrityTerminalOutcome::Aborted
505 )
506 && self.page_matches_counters(page)
507 }
508 (IntegrityJobState::Terminal { outcome, .. }, IntegrityJobReceipt::Abort(receipt)) => {
509 receipt.status == IntegrityAbortStatus::Terminal(outcome.clone())
510 && matches!(
511 outcome,
512 IntegrityTerminalOutcome::Expired | IntegrityTerminalOutcome::Aborted
513 )
514 }
515 _ => false,
516 };
517 if !receipt_matches_state
518 || self.last_receipt.receipt.page_sequence() != self.pages_completed
519 || !self.replay_key_matches_receipt()
520 || !self.terminal_outcome_matches_counts()
521 {
522 return Err(IntegrityJobError::CorruptProgressRecord);
523 }
524
525 Ok(())
526 }
527
528 fn page_matches_counters(&self, page: &DeepIntegrityPage) -> bool {
529 page.pages_completed == self.pages_completed
530 && page.findings_seen == self.findings_seen
531 && page.findings.len() <= MAX_INTEGRITY_RECEIPT_FINDINGS
532 && u64::try_from(page.findings.len()).is_ok_and(|count| count <= self.findings_seen)
533 && page.findings.iter().all(|finding| {
534 finding.value_path().is_none_or(|path| {
535 finding.kind() == IntegrityFindingKind::ConstraintViolation
536 && finding.constraint_id().is_some()
537 && finding.constraint_name().is_some()
538 && constraint_value_path_is_well_formed(path)
539 })
540 })
541 && page.blocked_verifier_families == self.blocked_verifier_families
542 }
543
544 fn replay_key_matches_receipt(&self) -> bool {
545 match self.last_receipt.replay_key {
546 IntegrityReceiptReplayKey::Start => {
547 self.pages_completed == 0
548 && self.last_receipt.receipt.page_sequence() == 0
549 && matches!(
550 &self.last_receipt.receipt,
551 IntegrityJobReceipt::Page(DeepIntegrityPage {
552 status: DeepIntegrityPageStatus::InProgress,
553 ..
554 })
555 )
556 }
557 IntegrityReceiptReplayKey::Continue {
558 acknowledged_sequence,
559 } => acknowledged_sequence
560 .checked_add(1)
561 .is_some_and(|sequence| sequence == self.last_receipt.receipt.page_sequence()),
562 }
563 }
564
565 const fn terminal_outcome_matches_counts(&self) -> bool {
566 match &self.state {
567 IntegrityJobState::Terminal {
568 outcome: IntegrityTerminalOutcome::DeepCompleteClean,
569 ..
570 } => self.findings_seen == 0 && self.blocked_verifier_families.is_empty(),
571 IntegrityJobState::Terminal {
572 outcome: IntegrityTerminalOutcome::DeepCompleteWithFindings,
573 ..
574 } => self.findings_seen > 0 || !self.blocked_verifier_families.is_empty(),
575 _ => true,
576 }
577 }
578
579 fn checkpoint_is_well_formed(&self) -> bool {
580 match &self.checkpoint {
581 IntegrityCheckpoint::Rows(checkpoint) => row_checkpoint_is_well_formed(checkpoint),
582 IntegrityCheckpoint::Index {
583 ordinal,
584 checkpoint,
585 } => {
586 usize::try_from(*ordinal).is_ok_and(|ordinal| {
587 ordinal < self.captured_proof_vector.index_generation_count()
588 }) && index_checkpoint_is_well_formed(checkpoint)
589 }
590 IntegrityCheckpoint::ReverseRelation {
591 ordinal,
592 checkpoint,
593 } => {
594 usize::try_from(*ordinal).is_ok_and(|ordinal| {
595 ordinal < self.captured_proof_vector.relation_generation_count()
596 }) && reverse_checkpoint_is_well_formed(checkpoint)
597 }
598 IntegrityCheckpoint::Journal {
599 store_ordinal,
600 checkpoint,
601 } => usize::try_from(*store_ordinal)
602 .ok()
603 .and_then(|ordinal| self.captured_proof_vector.stores().get(ordinal))
604 .is_some_and(|proof| {
605 let (fold_sequence, next_append_sequence) = proof.journal_interval();
606 journal_checkpoint_is_well_formed(
607 checkpoint,
608 fold_sequence,
609 next_append_sequence,
610 )
611 }),
612 IntegrityCheckpoint::QuickMetadata | IntegrityCheckpoint::FinalProof => true,
613 }
614 }
615}
616
617fn constraint_value_path_is_well_formed(path: &ConstraintValuePath) -> bool {
618 let Some((first, remaining)) = path.components().split_first() else {
619 return false;
620 };
621 path.components().len() <= MAX_ACCEPTED_TARGET_PATH_COMPONENTS
622 && matches!(
623 first,
624 ConstraintValuePathComponent::RootField { field_id } if *field_id != 0
625 )
626 && remaining.iter().all(|component| match component {
627 ConstraintValuePathComponent::RootField { .. } => false,
628 ConstraintValuePathComponent::RecordMember {
629 composite_type_id,
630 member_id,
631 } => *composite_type_id != 0 && *member_id != 0,
632 ConstraintValuePathComponent::TupleElement {
633 composite_type_id, ..
634 }
635 | ConstraintValuePathComponent::Newtype { composite_type_id } => {
636 *composite_type_id != 0
637 }
638 ConstraintValuePathComponent::EnumVariant {
639 enum_type_id,
640 variant_id,
641 } => *enum_type_id != 0 && *variant_id != 0,
642 ConstraintValuePathComponent::ListElement { .. }
643 | ConstraintValuePathComponent::SetElement { .. }
644 | ConstraintValuePathComponent::MapEntryKey { .. }
645 | ConstraintValuePathComponent::MapEntryValue { .. } => true,
646 })
647}
648
649fn row_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
650 checkpoint.raw_data_key().is_ok()
651 && !matches!(
652 checkpoint,
653 PhysicalUnitCheckpoint::Within {
654 verifier_family: IntegrityVerifierFamily::IndexEntry
655 | IntegrityVerifierFamily::UniqueIndex
656 | IntegrityVerifierFamily::ReverseRelationEntry
657 | IntegrityVerifierFamily::JournalEnvelope
658 | IntegrityVerifierFamily::JournalBatchIdentity,
659 ..
660 }
661 )
662}
663
664fn index_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
665 checkpoint.raw_index_key().is_ok()
666 && !matches!(
667 checkpoint,
668 PhysicalUnitCheckpoint::Within {
669 verifier_family: IntegrityVerifierFamily::DataKey
670 | IntegrityVerifierFamily::RowEnvelope
671 | IntegrityVerifierFamily::FieldValue
672 | IntegrityVerifierFamily::PrimaryKey
673 | IntegrityVerifierFamily::IdentityState
674 | IntegrityVerifierFamily::ValidatedConstraints
675 | IntegrityVerifierFamily::ForwardIndex
676 | IntegrityVerifierFamily::Relation
677 | IntegrityVerifierFamily::ReverseRelationEntry
678 | IntegrityVerifierFamily::JournalEnvelope
679 | IntegrityVerifierFamily::JournalBatchIdentity,
680 ..
681 }
682 )
683}
684
685fn reverse_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
686 checkpoint.raw_index_key().is_ok()
687 && !matches!(
688 checkpoint,
689 PhysicalUnitCheckpoint::Within {
690 verifier_family: IntegrityVerifierFamily::DataKey
691 | IntegrityVerifierFamily::RowEnvelope
692 | IntegrityVerifierFamily::FieldValue
693 | IntegrityVerifierFamily::PrimaryKey
694 | IntegrityVerifierFamily::IdentityState
695 | IntegrityVerifierFamily::ValidatedConstraints
696 | IntegrityVerifierFamily::ForwardIndex
697 | IntegrityVerifierFamily::Relation
698 | IntegrityVerifierFamily::IndexEntry
699 | IntegrityVerifierFamily::UniqueIndex
700 | IntegrityVerifierFamily::JournalEnvelope
701 | IntegrityVerifierFamily::JournalBatchIdentity,
702 ..
703 }
704 )
705}
706
707const fn journal_checkpoint_is_well_formed(
708 checkpoint: &JournalInspectionCheckpoint,
709 fold_sequence: u64,
710 next_append_sequence: u64,
711) -> bool {
712 match checkpoint {
713 JournalInspectionCheckpoint::BeforeFirst => true,
714 JournalInspectionCheckpoint::BeforeBatch { sequence } => {
715 *sequence > fold_sequence && *sequence < next_append_sequence
716 }
717 JournalInspectionCheckpoint::CheckingBatchIdentity {
718 sequence,
719 next_prior_sequence,
720 ..
721 } => {
722 *sequence > fold_sequence
723 && *sequence < next_append_sequence
724 && *next_prior_sequence > fold_sequence
725 && *next_prior_sequence < *sequence
726 }
727 JournalInspectionCheckpoint::AfterBatch { sequence } => {
728 *sequence >= fold_sequence && *sequence < next_append_sequence
729 }
730 }
731}
732
733fn strictly_sorted_unique(values: &[IntegrityVerifierFamily]) -> bool {
734 values.windows(2).all(|pair| pair[0] < pair[1])
735}
736
737#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
740#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
741pub enum IntegrityJobError {
742 CapacityExceeded,
744
745 CorruptProgressHeader,
747
748 CorruptProgressRecord,
750
751 CounterExhausted,
753
754 EntityIdentityMismatch,
756
757 IncompatibleProgressFormat,
759
760 Internal,
762
763 InvalidEntityIdentity,
765
766 InvalidJobId,
768
769 InvalidOwner,
771
772 InvalidSubmissionKey,
774
775 JobIncarnationMismatch,
777
778 JobNotFound,
780
781 JobOwnerMismatch,
783
784 StaleAcknowledgement,
786
787 StartInvalidated,
789
790 SubmissionAlreadyAdvanced,
792
793 SubmissionConflict,
795}
796
797#[derive(Debug)]
800pub enum IntegrityDeepError {
801 Internal(crate::error::InternalError),
803
804 Job(IntegrityJobError),
806
807 Uninspectable(IntegrityAuthorityDiagnostic),
809}
810
811impl IntegrityDeepError {
812 pub(in crate::db) fn from_plan_load(error: InternalError) -> Self {
814 match IntegrityTerminalOutcome::from_internal(&error) {
815 IntegrityTerminalOutcome::Uninspectable(diagnostic) => Self::Uninspectable(diagnostic),
816 _ => Self::Internal(error),
819 }
820 }
821}
822
823impl From<IntegrityJobError> for IntegrityDeepError {
824 fn from(error: IntegrityJobError) -> Self {
825 Self::Job(error)
826 }
827}
828
829impl From<crate::error::InternalError> for IntegrityDeepError {
830 fn from(error: crate::error::InternalError) -> Self {
831 Self::Internal(error)
832 }
833}
834
835#[cfg(test)]
836mod tests {
837 use super::*;
838 use crate::error::{ErrorClass, ErrorOrigin};
839 use icydb_diagnostic_code::DiagnosticExecutionBudgetResource as Resource;
840
841 #[test]
842 fn inspection_resource_failures_keep_their_typed_terminal_and_wire_code() {
843 use crate::db::{
844 resource::budget::MaintenanceConstructionBudget,
845 resource::construction::ConstructionBudget,
846 };
847 let work = MaintenanceConstructionBudget::with_limit_for_tests(Resource::TemporaryBytes, 0);
848 for error in [
849 work.charge(Resource::TemporaryBytes, 1).unwrap_err(),
850 InternalError::relation_budget_exceeded(Resource::NestedValueSteps, 10, 11),
851 InternalError::relation_budget_exceeded(Resource::TemporaryBytes, 10, 11),
852 InternalError::page_unit_too_large(Resource::TemporaryBytes, 10, 11),
853 ] {
854 let expected = error.diagnostic();
855 let outcome = IntegrityTerminalOutcome::from_internal(&error);
856 let IntegrityTerminalOutcome::ResourceLimited(diagnostic) = &outcome else {
857 panic!("resource exhaustion must not become an authority failure");
858 };
859 assert_eq!(
860 diagnostic.diagnostic_code(),
861 error.diagnostic_code().error_code().raw(),
862 );
863 let bytes = icydb_schema::wire::encode_one(&outcome).expect("terminal should encode");
864 let decoded: IntegrityTerminalOutcome =
865 icydb_schema::wire::decode_one(&bytes).expect("terminal should decode");
866 assert_eq!(decoded, outcome);
867 let IntegrityDeepError::Internal(error) = IntegrityDeepError::from_plan_load(error)
868 else {
869 panic!("plan-load exhaustion must retain its operational error");
870 };
871 assert_eq!(error.diagnostic(), expected);
872 }
873 }
874
875 #[test]
876 fn inspection_authority_failures_are_not_classified_as_exhaustion() {
877 for class in [
878 ErrorClass::Corruption,
879 ErrorClass::IncompatiblePersistedFormat,
880 ErrorClass::InvariantViolation,
881 ErrorClass::Unsupported,
882 ErrorClass::NotFound,
883 ErrorClass::Conflict,
884 ErrorClass::Internal,
885 ] {
886 let error = InternalError::classified(class, ErrorOrigin::Store);
887 assert_eq!(
888 IntegrityTerminalOutcome::from_internal(&error),
889 IntegrityTerminalOutcome::Uninspectable(
890 IntegrityAuthorityDiagnostic::from_internal(&error),
891 ),
892 );
893 let expected = IntegrityAuthorityDiagnostic::from_internal(&error);
894 let IntegrityDeepError::Uninspectable(diagnostic) =
895 IntegrityDeepError::from_plan_load(error)
896 else {
897 panic!("invalid authority keeps its established plan-load classification");
898 };
899 assert_eq!(diagnostic, expected);
900 }
901 }
902
903 #[test]
904 fn public_job_inputs_revalidate_after_wire_decode() {
905 let owner_bytes = icydb_schema::wire::encode_one(IntegrityJobOwner(String::new()))
906 .expect("owner should encode");
907 let owner: IntegrityJobOwner =
908 icydb_schema::wire::decode_one(&owner_bytes).expect("owner should decode");
909 assert_eq!(owner.validate(), Err(IntegrityJobError::InvalidOwner));
910
911 let submission_bytes =
912 icydb_schema::wire::encode_one(IntegritySubmissionKey(String::new()))
913 .expect("submission key should encode");
914 let submission: IntegritySubmissionKey = icydb_schema::wire::decode_one(&submission_bytes)
915 .expect("submission key should decode");
916 assert_eq!(
917 submission.validate(),
918 Err(IntegrityJobError::InvalidSubmissionKey),
919 );
920
921 let job_id_bytes =
922 icydb_schema::wire::encode_one(IntegrityJobId([0; 32])).expect("job id should encode");
923 let job_id: IntegrityJobId =
924 icydb_schema::wire::decode_one(&job_id_bytes).expect("job id should decode");
925 assert_eq!(job_id.validate(), Err(IntegrityJobError::InvalidJobId),);
926 }
927
928 #[test]
929 fn persisted_constraint_value_paths_require_current_accepted_id_shape() {
930 let valid = ConstraintValuePath::new(vec![
931 ConstraintValuePathComponent::RootField { field_id: 1 },
932 ConstraintValuePathComponent::RecordMember {
933 composite_type_id: 2,
934 member_id: 3,
935 },
936 ConstraintValuePathComponent::ListElement { index: 0 },
937 ]);
938 assert!(constraint_value_path_is_well_formed(&valid));
939
940 for malformed in [
941 ConstraintValuePath::new(Vec::new()),
942 ConstraintValuePath::new(vec![ConstraintValuePathComponent::RootField {
943 field_id: 0,
944 }]),
945 ConstraintValuePath::new(vec![
946 ConstraintValuePathComponent::RootField { field_id: 1 },
947 ConstraintValuePathComponent::RootField { field_id: 2 },
948 ]),
949 ConstraintValuePath::new(vec![
950 ConstraintValuePathComponent::RootField { field_id: 1 },
951 ConstraintValuePathComponent::Newtype {
952 composite_type_id: 0,
953 },
954 ]),
955 ] {
956 assert!(!constraint_value_path_is_well_formed(&malformed));
957 }
958 }
959
960 #[test]
961 fn public_job_id_hex_round_trip_is_exact_and_fail_closed() {
962 let mut bytes = [0_u8; 32];
963 bytes[0] = 0x01;
964 bytes[31] = 0xfe;
965 let job_id = IntegrityJobId::try_from_bytes(bytes).expect("job id should admit");
966 let encoded = job_id.to_hex();
967
968 assert_eq!(encoded.len(), 64);
969 assert_eq!(IntegrityJobId::try_from_hex(encoded.as_str()), Ok(job_id),);
970 assert_eq!(
971 IntegrityJobId::try_from_hex(encoded.to_uppercase().as_str()),
972 Ok(job_id),
973 );
974 for malformed in [
975 "",
976 "01",
977 "0000000000000000000000000000000000000000000000000000000000000000",
978 "g001000000000000000000000000000000000000000000000000000000000000",
979 ] {
980 assert_eq!(
981 IntegrityJobId::try_from_hex(malformed),
982 Err(IntegrityJobError::InvalidJobId),
983 );
984 }
985 }
986
987 #[test]
988 fn persisted_checkpoint_families_stay_phase_owned() {
989 let journal_in_row = PhysicalUnitCheckpoint::Within {
990 physical_key: vec![1],
991 verifier_family: IntegrityVerifierFamily::JournalEnvelope,
992 ordinal: 0,
993 };
994 let row_in_index = PhysicalUnitCheckpoint::Within {
995 physical_key: vec![1],
996 verifier_family: IntegrityVerifierFamily::FieldValue,
997 ordinal: 0,
998 };
999 let reverse_in_reverse = PhysicalUnitCheckpoint::Within {
1000 physical_key: vec![1],
1001 verifier_family: IntegrityVerifierFamily::ReverseRelationEntry,
1002 ordinal: 0,
1003 };
1004
1005 assert!(!row_checkpoint_is_well_formed(&journal_in_row));
1006 assert!(!index_checkpoint_is_well_formed(&row_in_index));
1007 assert!(reverse_checkpoint_is_well_formed(&reverse_in_reverse));
1008 }
1009
1010 #[test]
1011 fn persisted_journal_checkpoint_cannot_skip_the_captured_tail_interval() {
1012 assert!(journal_checkpoint_is_well_formed(
1013 &JournalInspectionCheckpoint::BeforeFirst,
1014 4,
1015 8,
1016 ));
1017 assert!(journal_checkpoint_is_well_formed(
1018 &JournalInspectionCheckpoint::BeforeBatch { sequence: 7 },
1019 4,
1020 8,
1021 ));
1022 assert!(journal_checkpoint_is_well_formed(
1023 &JournalInspectionCheckpoint::AfterBatch { sequence: 4 },
1024 4,
1025 8,
1026 ));
1027 assert!(!journal_checkpoint_is_well_formed(
1028 &JournalInspectionCheckpoint::BeforeBatch { sequence: 4 },
1029 4,
1030 8,
1031 ));
1032 assert!(!journal_checkpoint_is_well_formed(
1033 &JournalInspectionCheckpoint::AfterBatch { sequence: 8 },
1034 4,
1035 8,
1036 ));
1037 assert!(!journal_checkpoint_is_well_formed(
1038 &JournalInspectionCheckpoint::CheckingBatchIdentity {
1039 sequence: 7,
1040 batch_id: [1; 16],
1041 next_prior_sequence: 4,
1042 },
1043 4,
1044 8,
1045 ));
1046 }
1047}