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 From<IntegrityJobError> for IntegrityDeepError {
809 fn from(error: IntegrityJobError) -> Self {
810 Self::Job(error)
811 }
812}
813
814impl From<crate::error::InternalError> for IntegrityDeepError {
815 fn from(error: crate::error::InternalError) -> Self {
816 Self::Internal(error)
817 }
818}
819
820#[cfg(test)]
821mod tests {
822 use super::*;
823 use crate::error::{ErrorClass, ErrorOrigin};
824 use icydb_diagnostic_code::DiagnosticExecutionBudgetResource as Resource;
825
826 #[test]
827 fn inspection_resource_failures_keep_their_typed_terminal_and_wire_code() {
828 for error in [
829 InternalError::relation_budget_exceeded(Resource::NestedValueSteps, 10, 11),
830 InternalError::relation_budget_exceeded(Resource::TemporaryBytes, 10, 11),
831 InternalError::page_unit_too_large(Resource::TemporaryBytes, 10, 11),
832 ] {
833 let outcome = IntegrityTerminalOutcome::from_internal(&error);
834 let IntegrityTerminalOutcome::ResourceLimited(diagnostic) = &outcome else {
835 panic!("resource exhaustion must not become an authority failure");
836 };
837 assert_eq!(
838 diagnostic.diagnostic_code(),
839 error.diagnostic_code().error_code().raw(),
840 );
841 let bytes = candid::encode_one(&outcome).expect("terminal should encode");
842 let decoded: IntegrityTerminalOutcome =
843 candid::decode_one(&bytes).expect("terminal should decode");
844 assert_eq!(decoded, outcome);
845 }
846 }
847
848 #[test]
849 fn inspection_authority_failures_are_not_classified_as_exhaustion() {
850 for class in [
851 ErrorClass::Corruption,
852 ErrorClass::IncompatiblePersistedFormat,
853 ErrorClass::InvariantViolation,
854 ErrorClass::Unsupported,
855 ErrorClass::NotFound,
856 ErrorClass::Conflict,
857 ErrorClass::Internal,
858 ] {
859 let error = InternalError::classified(class, ErrorOrigin::Store);
860 assert_eq!(
861 IntegrityTerminalOutcome::from_internal(&error),
862 IntegrityTerminalOutcome::Uninspectable(
863 IntegrityAuthorityDiagnostic::from_internal(&error),
864 ),
865 );
866 }
867 }
868
869 #[test]
870 fn public_job_inputs_revalidate_after_wire_decode() {
871 let owner_bytes =
872 candid::encode_one(IntegrityJobOwner(String::new())).expect("owner should encode");
873 let owner: IntegrityJobOwner =
874 candid::decode_one(&owner_bytes).expect("owner should decode");
875 assert_eq!(owner.validate(), Err(IntegrityJobError::InvalidOwner));
876
877 let submission_bytes = candid::encode_one(IntegritySubmissionKey(String::new()))
878 .expect("submission key should encode");
879 let submission: IntegritySubmissionKey =
880 candid::decode_one(&submission_bytes).expect("submission key should decode");
881 assert_eq!(
882 submission.validate(),
883 Err(IntegrityJobError::InvalidSubmissionKey),
884 );
885
886 let job_id_bytes =
887 candid::encode_one(IntegrityJobId([0; 32])).expect("job id should encode");
888 let job_id: IntegrityJobId =
889 candid::decode_one(&job_id_bytes).expect("job id should decode");
890 assert_eq!(job_id.validate(), Err(IntegrityJobError::InvalidJobId),);
891 }
892
893 #[test]
894 fn persisted_constraint_value_paths_require_current_accepted_id_shape() {
895 let valid = ConstraintValuePath::new(vec![
896 ConstraintValuePathComponent::RootField { field_id: 1 },
897 ConstraintValuePathComponent::RecordMember {
898 composite_type_id: 2,
899 member_id: 3,
900 },
901 ConstraintValuePathComponent::ListElement { index: 0 },
902 ]);
903 assert!(constraint_value_path_is_well_formed(&valid));
904
905 for malformed in [
906 ConstraintValuePath::new(Vec::new()),
907 ConstraintValuePath::new(vec![ConstraintValuePathComponent::RootField {
908 field_id: 0,
909 }]),
910 ConstraintValuePath::new(vec![
911 ConstraintValuePathComponent::RootField { field_id: 1 },
912 ConstraintValuePathComponent::RootField { field_id: 2 },
913 ]),
914 ConstraintValuePath::new(vec![
915 ConstraintValuePathComponent::RootField { field_id: 1 },
916 ConstraintValuePathComponent::Newtype {
917 composite_type_id: 0,
918 },
919 ]),
920 ] {
921 assert!(!constraint_value_path_is_well_formed(&malformed));
922 }
923 }
924
925 #[test]
926 fn public_job_id_hex_round_trip_is_exact_and_fail_closed() {
927 let mut bytes = [0_u8; 32];
928 bytes[0] = 0x01;
929 bytes[31] = 0xfe;
930 let job_id = IntegrityJobId::try_from_bytes(bytes).expect("job id should admit");
931 let encoded = job_id.to_hex();
932
933 assert_eq!(encoded.len(), 64);
934 assert_eq!(IntegrityJobId::try_from_hex(encoded.as_str()), Ok(job_id),);
935 assert_eq!(
936 IntegrityJobId::try_from_hex(encoded.to_uppercase().as_str()),
937 Ok(job_id),
938 );
939 for malformed in [
940 "",
941 "01",
942 "0000000000000000000000000000000000000000000000000000000000000000",
943 "g001000000000000000000000000000000000000000000000000000000000000",
944 ] {
945 assert_eq!(
946 IntegrityJobId::try_from_hex(malformed),
947 Err(IntegrityJobError::InvalidJobId),
948 );
949 }
950 }
951
952 #[test]
953 fn persisted_checkpoint_families_stay_phase_owned() {
954 let journal_in_row = PhysicalUnitCheckpoint::Within {
955 physical_key: vec![1],
956 verifier_family: IntegrityVerifierFamily::JournalEnvelope,
957 ordinal: 0,
958 };
959 let row_in_index = PhysicalUnitCheckpoint::Within {
960 physical_key: vec![1],
961 verifier_family: IntegrityVerifierFamily::FieldValue,
962 ordinal: 0,
963 };
964 let reverse_in_reverse = PhysicalUnitCheckpoint::Within {
965 physical_key: vec![1],
966 verifier_family: IntegrityVerifierFamily::ReverseRelationEntry,
967 ordinal: 0,
968 };
969
970 assert!(!row_checkpoint_is_well_formed(&journal_in_row));
971 assert!(!index_checkpoint_is_well_formed(&row_in_index));
972 assert!(reverse_checkpoint_is_well_formed(&reverse_in_reverse));
973 }
974
975 #[test]
976 fn persisted_journal_checkpoint_cannot_skip_the_captured_tail_interval() {
977 assert!(journal_checkpoint_is_well_formed(
978 &JournalInspectionCheckpoint::BeforeFirst,
979 4,
980 8,
981 ));
982 assert!(journal_checkpoint_is_well_formed(
983 &JournalInspectionCheckpoint::BeforeBatch { sequence: 7 },
984 4,
985 8,
986 ));
987 assert!(journal_checkpoint_is_well_formed(
988 &JournalInspectionCheckpoint::AfterBatch { sequence: 4 },
989 4,
990 8,
991 ));
992 assert!(!journal_checkpoint_is_well_formed(
993 &JournalInspectionCheckpoint::BeforeBatch { sequence: 4 },
994 4,
995 8,
996 ));
997 assert!(!journal_checkpoint_is_well_formed(
998 &JournalInspectionCheckpoint::AfterBatch { sequence: 8 },
999 4,
1000 8,
1001 ));
1002 assert!(!journal_checkpoint_is_well_formed(
1003 &JournalInspectionCheckpoint::CheckingBatchIdentity {
1004 sequence: 7,
1005 batch_id: [1; 16],
1006 next_prior_sequence: 4,
1007 },
1008 4,
1009 8,
1010 ));
1011 }
1012}