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