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};
18use candid::CandidType;
19use serde::Deserialize;
20
21pub(in crate::db) const MAX_INTEGRITY_OWNER_BYTES: usize = 256;
22pub(in crate::db) const MAX_INTEGRITY_SUBMISSION_KEY_BYTES: usize = 256;
23const MAX_INTEGRITY_RECEIPT_FINDINGS: usize = 64;
24pub(in crate::db) const MAX_INTEGRITY_IN_PROGRESS_PAGES: u64 = u64::MAX - 1;
25
26#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
29pub struct IntegrityJobId([u8; 32]);
30
31impl IntegrityJobId {
32 pub fn try_from_bytes(bytes: [u8; 32]) -> Result<Self, IntegrityJobError> {
39 if bytes == [0; 32] {
40 return Err(IntegrityJobError::CorruptProgressRecord);
41 }
42 Ok(Self(bytes))
43 }
44
45 pub fn try_from_hex(value: &str) -> Result<Self, IntegrityJobError> {
52 if value.len() != 64 {
53 return Err(IntegrityJobError::InvalidJobId);
54 }
55
56 let mut bytes = [0_u8; 32];
57 for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
58 let high = decode_hex_nibble(pair[0]).ok_or(IntegrityJobError::InvalidJobId)?;
59 let low = decode_hex_nibble(pair[1]).ok_or(IntegrityJobError::InvalidJobId)?;
60 bytes[index] = (high << 4) | low;
61 }
62 if bytes == [0; 32] {
63 return Err(IntegrityJobError::InvalidJobId);
64 }
65
66 Ok(Self(bytes))
67 }
68
69 #[must_use]
71 pub const fn to_bytes(self) -> [u8; 32] {
72 self.0
73 }
74
75 #[must_use]
77 pub fn to_hex(self) -> String {
78 encode_hex_lower(&self.0)
79 }
80
81 pub(in crate::db) fn validate(self) -> Result<(), IntegrityJobError> {
83 if self.0 == [0; 32] {
84 return Err(IntegrityJobError::InvalidJobId);
85 }
86 Ok(())
87 }
88}
89
90const fn decode_hex_nibble(value: u8) -> Option<u8> {
91 match value {
92 b'0'..=b'9' => Some(value - b'0'),
93 b'a'..=b'f' => Some(value - b'a' + 10),
94 b'A'..=b'F' => Some(value - b'A' + 10),
95 _ => None,
96 }
97}
98
99#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
102pub struct IntegrityJobOwner(String);
103
104impl IntegrityJobOwner {
105 pub fn new(value: impl Into<String>) -> Result<Self, IntegrityJobError> {
112 let value = value.into();
113 if value.is_empty() || value.len() > MAX_INTEGRITY_OWNER_BYTES {
114 return Err(IntegrityJobError::InvalidOwner);
115 }
116 Ok(Self(value))
117 }
118
119 #[must_use]
121 pub const fn as_str(&self) -> &str {
122 self.0.as_str()
123 }
124
125 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
127 if self.0.is_empty() || self.0.len() > MAX_INTEGRITY_OWNER_BYTES {
128 return Err(IntegrityJobError::InvalidOwner);
129 }
130 Ok(())
131 }
132}
133
134#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
137pub struct IntegritySubmissionKey(String);
138
139impl IntegritySubmissionKey {
140 pub fn new(value: impl Into<String>) -> Result<Self, IntegrityJobError> {
147 let value = value.into();
148 if value.is_empty() || value.len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES {
149 return Err(IntegrityJobError::InvalidSubmissionKey);
150 }
151 Ok(Self(value))
152 }
153
154 #[must_use]
156 pub const fn as_str(&self) -> &str {
157 self.0.as_str()
158 }
159
160 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
162 if self.0.is_empty() || self.0.len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES {
163 return Err(IntegrityJobError::InvalidSubmissionKey);
164 }
165 Ok(())
166 }
167}
168
169#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
171pub(in crate::db) enum IntegrityCheckpoint {
172 QuickMetadata,
174 Rows(PhysicalUnitCheckpoint),
176 Index {
178 ordinal: u32,
179 checkpoint: PhysicalUnitCheckpoint,
180 },
181 ReverseRelation {
183 ordinal: u32,
184 checkpoint: PhysicalUnitCheckpoint,
185 },
186 Journal {
188 store_ordinal: u32,
189 checkpoint: JournalInspectionCheckpoint,
190 },
191 FinalProof,
193}
194
195impl IntegrityCheckpoint {
196 #[must_use]
198 pub(in crate::db) const fn phase(&self) -> IntegrityPhase {
199 match self {
200 Self::QuickMetadata => IntegrityPhase::QuickMetadata,
201 Self::Rows(_) => IntegrityPhase::Rows,
202 Self::Index { .. } => IntegrityPhase::IndexEntries,
203 Self::ReverseRelation { .. } => IntegrityPhase::ReverseRelations,
204 Self::Journal { .. } => IntegrityPhase::JournalTails,
205 Self::FinalProof => IntegrityPhase::FinalProofVectorCheck,
206 }
207 }
208}
209
210#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
213pub enum IntegrityPendingTerminal {
214 Expired,
216 Aborted,
218}
219
220#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
223pub enum IntegrityTerminalOutcome {
224 DeepCompleteClean,
226 DeepCompleteWithFindings,
228 Invalidated,
230 Uninspectable(IntegrityAuthorityDiagnostic),
232 ResourceLimited(IntegrityResourceDiagnostic),
234 Expired,
236 Aborted,
238}
239
240#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
242pub(in crate::db) enum IntegrityJobState {
243 InProgress,
245 TerminalPending(IntegrityPendingTerminal),
247 Terminal {
249 outcome: IntegrityTerminalOutcome,
250 receipt_acknowledged: bool,
251 },
252}
253
254#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
257pub enum DeepIntegrityPageStatus {
258 InProgress,
260 Terminal(IntegrityTerminalOutcome),
262}
263
264#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
267pub struct DeepIntegrityPage {
268 pub(super) job_id: IntegrityJobId,
269 pub(super) page_sequence: u64,
270 pub(super) phase: IntegrityPhase,
271 pub(super) status: DeepIntegrityPageStatus,
272 pub(super) pages_completed: u64,
273 pub(super) findings_seen: u64,
274 pub(super) findings: Vec<IntegrityFinding>,
275 pub(super) blocked_verifier_families: Vec<IntegrityVerifierFamily>,
276}
277
278impl DeepIntegrityPage {
279 #[must_use]
281 pub const fn job_id(&self) -> IntegrityJobId {
282 self.job_id
283 }
284
285 #[must_use]
287 pub const fn page_sequence(&self) -> u64 {
288 self.page_sequence
289 }
290
291 #[must_use]
293 pub const fn phase(&self) -> IntegrityPhase {
294 self.phase
295 }
296
297 #[must_use]
299 pub const fn status(&self) -> &DeepIntegrityPageStatus {
300 &self.status
301 }
302
303 #[must_use]
305 pub const fn pages_completed(&self) -> u64 {
306 self.pages_completed
307 }
308
309 #[must_use]
311 pub const fn findings_seen(&self) -> u64 {
312 self.findings_seen
313 }
314
315 #[must_use]
317 pub const fn findings(&self) -> &[IntegrityFinding] {
318 self.findings.as_slice()
319 }
320
321 #[must_use]
323 pub const fn blocked_verifier_families(&self) -> &[IntegrityVerifierFamily] {
324 self.blocked_verifier_families.as_slice()
325 }
326}
327
328#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
331pub enum IntegrityAbortStatus {
332 TerminationPending(IntegrityPendingTerminal),
334 Terminal(IntegrityTerminalOutcome),
336}
337
338#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
341pub struct IntegrityAbortReceipt {
342 pub(super) job_id: IntegrityJobId,
343 pub(super) page_sequence: u64,
344 pub(super) status: IntegrityAbortStatus,
345}
346
347impl IntegrityAbortReceipt {
348 #[must_use]
350 pub const fn job_id(&self) -> IntegrityJobId {
351 self.job_id
352 }
353
354 #[must_use]
356 pub const fn page_sequence(&self) -> u64 {
357 self.page_sequence
358 }
359
360 #[must_use]
362 pub const fn status(&self) -> &IntegrityAbortStatus {
363 &self.status
364 }
365}
366
367#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
370pub enum IntegrityJobReceipt {
371 Page(DeepIntegrityPage),
373 Abort(IntegrityAbortReceipt),
375}
376
377impl IntegrityJobReceipt {
378 #[must_use]
380 pub const fn job_id(&self) -> IntegrityJobId {
381 match self {
382 Self::Page(page) => page.job_id,
383 Self::Abort(receipt) => receipt.job_id,
384 }
385 }
386
387 #[must_use]
389 pub const fn page_sequence(&self) -> u64 {
390 match self {
391 Self::Page(page) => page.page_sequence,
392 Self::Abort(receipt) => receipt.page_sequence,
393 }
394 }
395}
396
397#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
399pub(in crate::db) enum IntegrityReceiptReplayKey {
400 Start,
402 Continue { acknowledged_sequence: u64 },
404}
405
406#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
408pub(in crate::db) struct IntegrityReceiptEnvelope {
409 pub(super) replay_key: IntegrityReceiptReplayKey,
410 pub(super) receipt: IntegrityJobReceipt,
411}
412
413#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
415pub(in crate::db) struct IntegrityJob {
416 pub(super) id: IntegrityJobId,
417 pub(super) database_incarnation_id: DatabaseIncarnationId,
418 pub(super) owner: IntegrityJobOwner,
419 pub(super) submission_key: IntegritySubmissionKey,
420 pub(super) entity: IntegrityEntityIdentity,
421 pub(super) accepted_schema_version: u32,
422 pub(super) accepted_schema_fingerprint: [u8; 16],
423 pub(super) inspection_plan_fingerprint: [u8; 32],
424 pub(super) checkpoint: IntegrityCheckpoint,
425 pub(super) captured_proof_vector: IntegrityProofVector,
426 pub(super) state: IntegrityJobState,
427 pub(super) lease_deadline_nanos: u64,
428 pub(super) findings_seen: u64,
429 pub(super) pages_completed: u64,
430 pub(super) blocked_verifier_families: Vec<IntegrityVerifierFamily>,
431 pub(super) last_receipt: IntegrityReceiptEnvelope,
432}
433
434impl IntegrityJob {
435 pub(super) fn validate(&self) -> Result<(), IntegrityJobError> {
437 if self.id != self.last_receipt.receipt.job_id()
438 || self.database_incarnation_id != self.captured_proof_vector.database_incarnation_id()
439 || self.accepted_schema_version != self.captured_proof_vector.accepted_schema_version()
440 || self.accepted_schema_fingerprint
441 != self.captured_proof_vector.accepted_schema_fingerprint()
442 || self.inspection_plan_fingerprint
443 != self.captured_proof_vector.inspection_plan_fingerprint()
444 || self.entity.entity_path().is_empty()
445 || self.entity.entity_path().len() > MAX_INTEGRITY_PATH_BYTES
446 || self.entity.store_path().is_empty()
447 || self.entity.store_path().len() > MAX_INTEGRITY_PATH_BYTES
448 || self.entity.entity_tag() == 0
449 || self.owner.as_str().is_empty()
450 || self.owner.as_str().len() > MAX_INTEGRITY_OWNER_BYTES
451 || self.submission_key.as_str().is_empty()
452 || self.submission_key.as_str().len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES
453 || self.accepted_schema_version == 0
454 || self.lease_deadline_nanos == 0
455 || matches!(
456 self.state,
457 IntegrityJobState::InProgress | IntegrityJobState::TerminalPending(_)
458 ) && self.pages_completed > MAX_INTEGRITY_IN_PROGRESS_PAGES
459 || !strictly_sorted_unique(&self.blocked_verifier_families)
460 || self.captured_proof_vector.validate().is_err()
461 || !self.checkpoint_is_well_formed()
462 {
463 return Err(IntegrityJobError::CorruptProgressRecord);
464 }
465
466 let receipt_matches_state = match (&self.state, &self.last_receipt.receipt) {
467 (
468 IntegrityJobState::InProgress | IntegrityJobState::TerminalPending(_),
469 IntegrityJobReceipt::Page(page),
470 ) => {
471 page.status == DeepIntegrityPageStatus::InProgress
472 && page.phase == self.checkpoint.phase()
473 && self.page_matches_counters(page)
474 }
475 (IntegrityJobState::Terminal { outcome, .. }, IntegrityJobReceipt::Page(page)) => {
476 page.status == DeepIntegrityPageStatus::Terminal(outcome.clone())
477 && page.phase == self.checkpoint.phase()
478 && !matches!(
479 outcome,
480 IntegrityTerminalOutcome::Expired | IntegrityTerminalOutcome::Aborted
481 )
482 && self.page_matches_counters(page)
483 }
484 (IntegrityJobState::Terminal { outcome, .. }, IntegrityJobReceipt::Abort(receipt)) => {
485 receipt.status == IntegrityAbortStatus::Terminal(outcome.clone())
486 && matches!(
487 outcome,
488 IntegrityTerminalOutcome::Expired | IntegrityTerminalOutcome::Aborted
489 )
490 }
491 _ => false,
492 };
493 if !receipt_matches_state
494 || self.last_receipt.receipt.page_sequence() != self.pages_completed
495 || !self.replay_key_matches_receipt()
496 || !self.terminal_outcome_matches_counts()
497 {
498 return Err(IntegrityJobError::CorruptProgressRecord);
499 }
500
501 Ok(())
502 }
503
504 fn page_matches_counters(&self, page: &DeepIntegrityPage) -> bool {
505 page.pages_completed == self.pages_completed
506 && page.findings_seen == self.findings_seen
507 && page.findings.len() <= MAX_INTEGRITY_RECEIPT_FINDINGS
508 && u64::try_from(page.findings.len()).is_ok_and(|count| count <= self.findings_seen)
509 && page.findings.iter().all(|finding| {
510 finding.value_path().is_none_or(|path| {
511 finding.kind() == IntegrityFindingKind::ConstraintViolation
512 && finding.constraint_id().is_some()
513 && finding.constraint_name().is_some()
514 && constraint_value_path_is_well_formed(path)
515 })
516 })
517 && page.blocked_verifier_families == self.blocked_verifier_families
518 }
519
520 fn replay_key_matches_receipt(&self) -> bool {
521 match self.last_receipt.replay_key {
522 IntegrityReceiptReplayKey::Start => {
523 self.pages_completed == 0
524 && self.last_receipt.receipt.page_sequence() == 0
525 && matches!(
526 &self.last_receipt.receipt,
527 IntegrityJobReceipt::Page(DeepIntegrityPage {
528 status: DeepIntegrityPageStatus::InProgress,
529 ..
530 })
531 )
532 }
533 IntegrityReceiptReplayKey::Continue {
534 acknowledged_sequence,
535 } => acknowledged_sequence
536 .checked_add(1)
537 .is_some_and(|sequence| sequence == self.last_receipt.receipt.page_sequence()),
538 }
539 }
540
541 const fn terminal_outcome_matches_counts(&self) -> bool {
542 match &self.state {
543 IntegrityJobState::Terminal {
544 outcome: IntegrityTerminalOutcome::DeepCompleteClean,
545 ..
546 } => self.findings_seen == 0 && self.blocked_verifier_families.is_empty(),
547 IntegrityJobState::Terminal {
548 outcome: IntegrityTerminalOutcome::DeepCompleteWithFindings,
549 ..
550 } => self.findings_seen > 0 || !self.blocked_verifier_families.is_empty(),
551 _ => true,
552 }
553 }
554
555 fn checkpoint_is_well_formed(&self) -> bool {
556 match &self.checkpoint {
557 IntegrityCheckpoint::Rows(checkpoint) => row_checkpoint_is_well_formed(checkpoint),
558 IntegrityCheckpoint::Index {
559 ordinal,
560 checkpoint,
561 } => {
562 usize::try_from(*ordinal).is_ok_and(|ordinal| {
563 ordinal < self.captured_proof_vector.index_generation_count()
564 }) && index_checkpoint_is_well_formed(checkpoint)
565 }
566 IntegrityCheckpoint::ReverseRelation {
567 ordinal,
568 checkpoint,
569 } => {
570 usize::try_from(*ordinal).is_ok_and(|ordinal| {
571 ordinal < self.captured_proof_vector.relation_generation_count()
572 }) && reverse_checkpoint_is_well_formed(checkpoint)
573 }
574 IntegrityCheckpoint::Journal {
575 store_ordinal,
576 checkpoint,
577 } => usize::try_from(*store_ordinal)
578 .ok()
579 .and_then(|ordinal| self.captured_proof_vector.stores().get(ordinal))
580 .is_some_and(|proof| {
581 let (fold_sequence, next_append_sequence) = proof.journal_interval();
582 journal_checkpoint_is_well_formed(
583 checkpoint,
584 fold_sequence,
585 next_append_sequence,
586 )
587 }),
588 IntegrityCheckpoint::QuickMetadata | IntegrityCheckpoint::FinalProof => true,
589 }
590 }
591}
592
593fn constraint_value_path_is_well_formed(path: &ConstraintValuePath) -> bool {
594 let Some((first, remaining)) = path.components().split_first() else {
595 return false;
596 };
597 path.components().len() <= MAX_ACCEPTED_TARGET_PATH_COMPONENTS
598 && matches!(
599 first,
600 ConstraintValuePathComponent::RootField { field_id } if *field_id != 0
601 )
602 && remaining.iter().all(|component| match component {
603 ConstraintValuePathComponent::RootField { .. } => false,
604 ConstraintValuePathComponent::RecordMember {
605 composite_type_id,
606 member_id,
607 } => *composite_type_id != 0 && *member_id != 0,
608 ConstraintValuePathComponent::TupleElement {
609 composite_type_id, ..
610 }
611 | ConstraintValuePathComponent::Newtype { composite_type_id } => {
612 *composite_type_id != 0
613 }
614 ConstraintValuePathComponent::EnumVariant {
615 enum_type_id,
616 variant_id,
617 } => *enum_type_id != 0 && *variant_id != 0,
618 ConstraintValuePathComponent::ListElement { .. }
619 | ConstraintValuePathComponent::SetElement { .. }
620 | ConstraintValuePathComponent::MapEntryKey { .. }
621 | ConstraintValuePathComponent::MapEntryValue { .. } => true,
622 })
623}
624
625fn row_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
626 checkpoint.raw_data_key().is_ok()
627 && !matches!(
628 checkpoint,
629 PhysicalUnitCheckpoint::Within {
630 verifier_family: IntegrityVerifierFamily::IndexEntry
631 | IntegrityVerifierFamily::UniqueIndex
632 | IntegrityVerifierFamily::ReverseRelationEntry
633 | IntegrityVerifierFamily::JournalEnvelope
634 | IntegrityVerifierFamily::JournalBatchIdentity,
635 ..
636 }
637 )
638}
639
640fn index_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
641 checkpoint.raw_index_key().is_ok()
642 && !matches!(
643 checkpoint,
644 PhysicalUnitCheckpoint::Within {
645 verifier_family: IntegrityVerifierFamily::DataKey
646 | IntegrityVerifierFamily::RowEnvelope
647 | IntegrityVerifierFamily::FieldValue
648 | IntegrityVerifierFamily::PrimaryKey
649 | IntegrityVerifierFamily::IdentityState
650 | IntegrityVerifierFamily::ValidatedConstraints
651 | IntegrityVerifierFamily::ForwardIndex
652 | IntegrityVerifierFamily::Relation
653 | IntegrityVerifierFamily::ReverseRelationEntry
654 | IntegrityVerifierFamily::JournalEnvelope
655 | IntegrityVerifierFamily::JournalBatchIdentity,
656 ..
657 }
658 )
659}
660
661fn reverse_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
662 checkpoint.raw_index_key().is_ok()
663 && !matches!(
664 checkpoint,
665 PhysicalUnitCheckpoint::Within {
666 verifier_family: IntegrityVerifierFamily::DataKey
667 | IntegrityVerifierFamily::RowEnvelope
668 | IntegrityVerifierFamily::FieldValue
669 | IntegrityVerifierFamily::PrimaryKey
670 | IntegrityVerifierFamily::IdentityState
671 | IntegrityVerifierFamily::ValidatedConstraints
672 | IntegrityVerifierFamily::ForwardIndex
673 | IntegrityVerifierFamily::Relation
674 | IntegrityVerifierFamily::IndexEntry
675 | IntegrityVerifierFamily::UniqueIndex
676 | IntegrityVerifierFamily::JournalEnvelope
677 | IntegrityVerifierFamily::JournalBatchIdentity,
678 ..
679 }
680 )
681}
682
683const fn journal_checkpoint_is_well_formed(
684 checkpoint: &JournalInspectionCheckpoint,
685 fold_sequence: u64,
686 next_append_sequence: u64,
687) -> bool {
688 match checkpoint {
689 JournalInspectionCheckpoint::BeforeFirst => true,
690 JournalInspectionCheckpoint::BeforeBatch { sequence } => {
691 *sequence > fold_sequence && *sequence < next_append_sequence
692 }
693 JournalInspectionCheckpoint::CheckingBatchIdentity {
694 sequence,
695 next_prior_sequence,
696 ..
697 } => {
698 *sequence > fold_sequence
699 && *sequence < next_append_sequence
700 && *next_prior_sequence > fold_sequence
701 && *next_prior_sequence < *sequence
702 }
703 JournalInspectionCheckpoint::AfterBatch { sequence } => {
704 *sequence >= fold_sequence && *sequence < next_append_sequence
705 }
706 }
707}
708
709fn strictly_sorted_unique(values: &[IntegrityVerifierFamily]) -> bool {
710 values.windows(2).all(|pair| pair[0] < pair[1])
711}
712
713#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
716pub enum IntegrityJobError {
717 CapacityExceeded,
719
720 CorruptProgressHeader,
722
723 CorruptProgressRecord,
725
726 CounterExhausted,
728
729 EntityIdentityMismatch,
731
732 IncompatibleProgressFormat,
734
735 Internal,
737
738 InvalidEntityIdentity,
740
741 InvalidJobId,
743
744 InvalidOwner,
746
747 InvalidSubmissionKey,
749
750 JobIncarnationMismatch,
752
753 JobNotFound,
755
756 JobOwnerMismatch,
758
759 StaleAcknowledgement,
761
762 StartInvalidated,
764
765 SubmissionAlreadyAdvanced,
767
768 SubmissionConflict,
770}
771
772#[derive(Debug)]
775pub enum IntegrityDeepError {
776 Internal(crate::error::InternalError),
778
779 Job(IntegrityJobError),
781
782 Uninspectable(IntegrityAuthorityDiagnostic),
784}
785
786impl From<IntegrityJobError> for IntegrityDeepError {
787 fn from(error: IntegrityJobError) -> Self {
788 Self::Job(error)
789 }
790}
791
792impl From<crate::error::InternalError> for IntegrityDeepError {
793 fn from(error: crate::error::InternalError) -> Self {
794 Self::Internal(error)
795 }
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
801
802 #[test]
803 fn public_job_inputs_revalidate_after_wire_decode() {
804 let owner_bytes =
805 candid::encode_one(IntegrityJobOwner(String::new())).expect("owner should encode");
806 let owner: IntegrityJobOwner =
807 candid::decode_one(&owner_bytes).expect("owner should decode");
808 assert_eq!(owner.validate(), Err(IntegrityJobError::InvalidOwner));
809
810 let submission_bytes = candid::encode_one(IntegritySubmissionKey(String::new()))
811 .expect("submission key should encode");
812 let submission: IntegritySubmissionKey =
813 candid::decode_one(&submission_bytes).expect("submission key should decode");
814 assert_eq!(
815 submission.validate(),
816 Err(IntegrityJobError::InvalidSubmissionKey),
817 );
818
819 let job_id_bytes =
820 candid::encode_one(IntegrityJobId([0; 32])).expect("job id should encode");
821 let job_id: IntegrityJobId =
822 candid::decode_one(&job_id_bytes).expect("job id should decode");
823 assert_eq!(job_id.validate(), Err(IntegrityJobError::InvalidJobId),);
824 }
825
826 #[test]
827 fn persisted_constraint_value_paths_require_current_accepted_id_shape() {
828 let valid = ConstraintValuePath::new(vec![
829 ConstraintValuePathComponent::RootField { field_id: 1 },
830 ConstraintValuePathComponent::RecordMember {
831 composite_type_id: 2,
832 member_id: 3,
833 },
834 ConstraintValuePathComponent::ListElement { index: 0 },
835 ]);
836 assert!(constraint_value_path_is_well_formed(&valid));
837
838 for malformed in [
839 ConstraintValuePath::new(Vec::new()),
840 ConstraintValuePath::new(vec![ConstraintValuePathComponent::RootField {
841 field_id: 0,
842 }]),
843 ConstraintValuePath::new(vec![
844 ConstraintValuePathComponent::RootField { field_id: 1 },
845 ConstraintValuePathComponent::RootField { field_id: 2 },
846 ]),
847 ConstraintValuePath::new(vec![
848 ConstraintValuePathComponent::RootField { field_id: 1 },
849 ConstraintValuePathComponent::Newtype {
850 composite_type_id: 0,
851 },
852 ]),
853 ] {
854 assert!(!constraint_value_path_is_well_formed(&malformed));
855 }
856 }
857
858 #[test]
859 fn public_job_id_hex_round_trip_is_exact_and_fail_closed() {
860 let mut bytes = [0_u8; 32];
861 bytes[0] = 0x01;
862 bytes[31] = 0xfe;
863 let job_id = IntegrityJobId::try_from_bytes(bytes).expect("job id should admit");
864 let encoded = job_id.to_hex();
865
866 assert_eq!(encoded.len(), 64);
867 assert_eq!(IntegrityJobId::try_from_hex(encoded.as_str()), Ok(job_id),);
868 assert_eq!(
869 IntegrityJobId::try_from_hex(encoded.to_uppercase().as_str()),
870 Ok(job_id),
871 );
872 for malformed in [
873 "",
874 "01",
875 "0000000000000000000000000000000000000000000000000000000000000000",
876 "g001000000000000000000000000000000000000000000000000000000000000",
877 ] {
878 assert_eq!(
879 IntegrityJobId::try_from_hex(malformed),
880 Err(IntegrityJobError::InvalidJobId),
881 );
882 }
883 }
884
885 #[test]
886 fn persisted_checkpoint_families_stay_phase_owned() {
887 let journal_in_row = PhysicalUnitCheckpoint::Within {
888 physical_key: vec![1],
889 verifier_family: IntegrityVerifierFamily::JournalEnvelope,
890 ordinal: 0,
891 };
892 let row_in_index = PhysicalUnitCheckpoint::Within {
893 physical_key: vec![1],
894 verifier_family: IntegrityVerifierFamily::FieldValue,
895 ordinal: 0,
896 };
897 let reverse_in_reverse = PhysicalUnitCheckpoint::Within {
898 physical_key: vec![1],
899 verifier_family: IntegrityVerifierFamily::ReverseRelationEntry,
900 ordinal: 0,
901 };
902
903 assert!(!row_checkpoint_is_well_formed(&journal_in_row));
904 assert!(!index_checkpoint_is_well_formed(&row_in_index));
905 assert!(reverse_checkpoint_is_well_formed(&reverse_in_reverse));
906 }
907
908 #[test]
909 fn persisted_journal_checkpoint_cannot_skip_the_captured_tail_interval() {
910 assert!(journal_checkpoint_is_well_formed(
911 &JournalInspectionCheckpoint::BeforeFirst,
912 4,
913 8,
914 ));
915 assert!(journal_checkpoint_is_well_formed(
916 &JournalInspectionCheckpoint::BeforeBatch { sequence: 7 },
917 4,
918 8,
919 ));
920 assert!(journal_checkpoint_is_well_formed(
921 &JournalInspectionCheckpoint::AfterBatch { sequence: 4 },
922 4,
923 8,
924 ));
925 assert!(!journal_checkpoint_is_well_formed(
926 &JournalInspectionCheckpoint::BeforeBatch { sequence: 4 },
927 4,
928 8,
929 ));
930 assert!(!journal_checkpoint_is_well_formed(
931 &JournalInspectionCheckpoint::AfterBatch { sequence: 8 },
932 4,
933 8,
934 ));
935 assert!(!journal_checkpoint_is_well_formed(
936 &JournalInspectionCheckpoint::CheckingBatchIdentity {
937 sequence: 7,
938 batch_id: [1; 16],
939 next_prior_sequence: 4,
940 },
941 4,
942 8,
943 ));
944 }
945}