1use crate::db::{
7 codec::hex::encode_hex_lower,
8 integrity::{
9 DatabaseIncarnationId, IntegrityAuthorityDiagnostic, IntegrityEntityIdentity,
10 IntegrityFinding, IntegrityPhase, IntegrityProofVector, IntegrityResourceDiagnostic,
11 IntegrityVerifierFamily, MAX_INTEGRITY_PATH_BYTES, PhysicalUnitCheckpoint,
12 StorageTraversalCorruption,
13 },
14 journal::JournalInspectionCheckpoint,
15};
16use candid::CandidType;
17use serde::Deserialize;
18
19pub(in crate::db) const MAX_INTEGRITY_OWNER_BYTES: usize = 256;
20pub(in crate::db) const MAX_INTEGRITY_SUBMISSION_KEY_BYTES: usize = 256;
21const MAX_INTEGRITY_RECEIPT_FINDINGS: usize = 64;
22pub(in crate::db) const MAX_INTEGRITY_IN_PROGRESS_PAGES: u64 = u64::MAX - 1;
23
24#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
27pub struct IntegrityJobId([u8; 32]);
28
29impl IntegrityJobId {
30 pub fn try_from_bytes(bytes: [u8; 32]) -> Result<Self, IntegrityJobError> {
37 if bytes == [0; 32] {
38 return Err(IntegrityJobError::CorruptProgressRecord);
39 }
40 Ok(Self(bytes))
41 }
42
43 pub fn try_from_hex(value: &str) -> Result<Self, IntegrityJobError> {
50 if value.len() != 64 {
51 return Err(IntegrityJobError::InvalidJobId);
52 }
53
54 let mut bytes = [0_u8; 32];
55 for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
56 let high = decode_hex_nibble(pair[0]).ok_or(IntegrityJobError::InvalidJobId)?;
57 let low = decode_hex_nibble(pair[1]).ok_or(IntegrityJobError::InvalidJobId)?;
58 bytes[index] = (high << 4) | low;
59 }
60 if bytes == [0; 32] {
61 return Err(IntegrityJobError::InvalidJobId);
62 }
63
64 Ok(Self(bytes))
65 }
66
67 #[must_use]
69 pub const fn to_bytes(self) -> [u8; 32] {
70 self.0
71 }
72
73 #[must_use]
75 pub fn to_hex(self) -> String {
76 encode_hex_lower(&self.0)
77 }
78
79 pub(in crate::db) fn validate(self) -> Result<(), IntegrityJobError> {
81 if self.0 == [0; 32] {
82 return Err(IntegrityJobError::InvalidJobId);
83 }
84 Ok(())
85 }
86}
87
88const fn decode_hex_nibble(value: u8) -> Option<u8> {
89 match value {
90 b'0'..=b'9' => Some(value - b'0'),
91 b'a'..=b'f' => Some(value - b'a' + 10),
92 b'A'..=b'F' => Some(value - b'A' + 10),
93 _ => None,
94 }
95}
96
97#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
100pub struct IntegrityJobOwner(String);
101
102impl IntegrityJobOwner {
103 pub fn new(value: impl Into<String>) -> Result<Self, IntegrityJobError> {
110 let value = value.into();
111 if value.is_empty() || value.len() > MAX_INTEGRITY_OWNER_BYTES {
112 return Err(IntegrityJobError::InvalidOwner);
113 }
114 Ok(Self(value))
115 }
116
117 #[must_use]
119 pub const fn as_str(&self) -> &str {
120 self.0.as_str()
121 }
122
123 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
125 if self.0.is_empty() || self.0.len() > MAX_INTEGRITY_OWNER_BYTES {
126 return Err(IntegrityJobError::InvalidOwner);
127 }
128 Ok(())
129 }
130}
131
132#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
135pub struct IntegritySubmissionKey(String);
136
137impl IntegritySubmissionKey {
138 pub fn new(value: impl Into<String>) -> Result<Self, IntegrityJobError> {
145 let value = value.into();
146 if value.is_empty() || value.len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES {
147 return Err(IntegrityJobError::InvalidSubmissionKey);
148 }
149 Ok(Self(value))
150 }
151
152 #[must_use]
154 pub const fn as_str(&self) -> &str {
155 self.0.as_str()
156 }
157
158 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
160 if self.0.is_empty() || self.0.len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES {
161 return Err(IntegrityJobError::InvalidSubmissionKey);
162 }
163 Ok(())
164 }
165}
166
167#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
169pub(in crate::db) enum IntegrityCheckpoint {
170 QuickMetadata,
172 Rows(PhysicalUnitCheckpoint),
174 Index {
176 ordinal: u32,
177 checkpoint: PhysicalUnitCheckpoint,
178 },
179 ReverseRelation {
181 ordinal: u32,
182 checkpoint: PhysicalUnitCheckpoint,
183 },
184 Journal {
186 store_ordinal: u32,
187 checkpoint: JournalInspectionCheckpoint,
188 },
189 FinalProof,
191}
192
193impl IntegrityCheckpoint {
194 #[must_use]
196 pub(in crate::db) const fn phase(&self) -> IntegrityPhase {
197 match self {
198 Self::QuickMetadata => IntegrityPhase::QuickMetadata,
199 Self::Rows(_) => IntegrityPhase::Rows,
200 Self::Index { .. } => IntegrityPhase::IndexEntries,
201 Self::ReverseRelation { .. } => IntegrityPhase::ReverseRelations,
202 Self::Journal { .. } => IntegrityPhase::JournalTails,
203 Self::FinalProof => IntegrityPhase::FinalProofVectorCheck,
204 }
205 }
206}
207
208#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
211pub enum IntegrityPendingTerminal {
212 Expired,
214 Aborted,
216}
217
218#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
221pub enum IntegrityTerminalOutcome {
222 DeepCompleteClean,
224 DeepCompleteWithFindings,
226 Invalidated,
228 Uninspectable(IntegrityAuthorityDiagnostic),
230 UninspectableStorage(StorageTraversalCorruption),
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.blocked_verifier_families == self.blocked_verifier_families
510 }
511
512 fn replay_key_matches_receipt(&self) -> bool {
513 match self.last_receipt.replay_key {
514 IntegrityReceiptReplayKey::Start => {
515 self.pages_completed == 0
516 && self.last_receipt.receipt.page_sequence() == 0
517 && matches!(
518 &self.last_receipt.receipt,
519 IntegrityJobReceipt::Page(DeepIntegrityPage {
520 status: DeepIntegrityPageStatus::InProgress,
521 ..
522 })
523 )
524 }
525 IntegrityReceiptReplayKey::Continue {
526 acknowledged_sequence,
527 } => acknowledged_sequence
528 .checked_add(1)
529 .is_some_and(|sequence| sequence == self.last_receipt.receipt.page_sequence()),
530 }
531 }
532
533 const fn terminal_outcome_matches_counts(&self) -> bool {
534 match &self.state {
535 IntegrityJobState::Terminal {
536 outcome: IntegrityTerminalOutcome::DeepCompleteClean,
537 ..
538 } => self.findings_seen == 0 && self.blocked_verifier_families.is_empty(),
539 IntegrityJobState::Terminal {
540 outcome: IntegrityTerminalOutcome::DeepCompleteWithFindings,
541 ..
542 } => self.findings_seen > 0 || !self.blocked_verifier_families.is_empty(),
543 _ => true,
544 }
545 }
546
547 fn checkpoint_is_well_formed(&self) -> bool {
548 match &self.checkpoint {
549 IntegrityCheckpoint::Rows(checkpoint) => row_checkpoint_is_well_formed(checkpoint),
550 IntegrityCheckpoint::Index {
551 ordinal,
552 checkpoint,
553 } => {
554 usize::try_from(*ordinal).is_ok_and(|ordinal| {
555 ordinal < self.captured_proof_vector.index_generation_count()
556 }) && index_checkpoint_is_well_formed(checkpoint)
557 }
558 IntegrityCheckpoint::ReverseRelation {
559 ordinal,
560 checkpoint,
561 } => {
562 usize::try_from(*ordinal).is_ok_and(|ordinal| {
563 ordinal < self.captured_proof_vector.relation_generation_count()
564 }) && reverse_checkpoint_is_well_formed(checkpoint)
565 }
566 IntegrityCheckpoint::Journal {
567 store_ordinal,
568 checkpoint,
569 } => usize::try_from(*store_ordinal)
570 .ok()
571 .and_then(|ordinal| self.captured_proof_vector.stores().get(ordinal))
572 .is_some_and(|proof| {
573 let (fold_sequence, next_append_sequence) = proof.journal_interval();
574 journal_checkpoint_is_well_formed(
575 checkpoint,
576 fold_sequence,
577 next_append_sequence,
578 )
579 }),
580 IntegrityCheckpoint::QuickMetadata | IntegrityCheckpoint::FinalProof => true,
581 }
582 }
583}
584
585fn row_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
586 checkpoint.raw_data_key().is_ok()
587 && !matches!(
588 checkpoint,
589 PhysicalUnitCheckpoint::Within {
590 verifier_family: IntegrityVerifierFamily::IndexEntry
591 | IntegrityVerifierFamily::UniqueIndex
592 | IntegrityVerifierFamily::ReverseRelationEntry
593 | IntegrityVerifierFamily::JournalEnvelope
594 | IntegrityVerifierFamily::JournalBatchIdentity,
595 ..
596 }
597 )
598}
599
600fn index_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
601 checkpoint.raw_index_key().is_ok()
602 && !matches!(
603 checkpoint,
604 PhysicalUnitCheckpoint::Within {
605 verifier_family: IntegrityVerifierFamily::DataKey
606 | IntegrityVerifierFamily::RowEnvelope
607 | IntegrityVerifierFamily::FieldValue
608 | IntegrityVerifierFamily::PrimaryKey
609 | IntegrityVerifierFamily::ValidatedConstraints
610 | IntegrityVerifierFamily::ForwardIndex
611 | IntegrityVerifierFamily::Relation
612 | IntegrityVerifierFamily::ReverseRelationEntry
613 | IntegrityVerifierFamily::JournalEnvelope
614 | IntegrityVerifierFamily::JournalBatchIdentity,
615 ..
616 }
617 )
618}
619
620fn reverse_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
621 checkpoint.raw_index_key().is_ok()
622 && !matches!(
623 checkpoint,
624 PhysicalUnitCheckpoint::Within {
625 verifier_family: IntegrityVerifierFamily::DataKey
626 | IntegrityVerifierFamily::RowEnvelope
627 | IntegrityVerifierFamily::FieldValue
628 | IntegrityVerifierFamily::PrimaryKey
629 | IntegrityVerifierFamily::ValidatedConstraints
630 | IntegrityVerifierFamily::ForwardIndex
631 | IntegrityVerifierFamily::Relation
632 | IntegrityVerifierFamily::IndexEntry
633 | IntegrityVerifierFamily::UniqueIndex
634 | IntegrityVerifierFamily::JournalEnvelope
635 | IntegrityVerifierFamily::JournalBatchIdentity,
636 ..
637 }
638 )
639}
640
641const fn journal_checkpoint_is_well_formed(
642 checkpoint: &JournalInspectionCheckpoint,
643 fold_sequence: u64,
644 next_append_sequence: u64,
645) -> bool {
646 match checkpoint {
647 JournalInspectionCheckpoint::BeforeFirst => true,
648 JournalInspectionCheckpoint::BeforeBatch { sequence } => {
649 *sequence > fold_sequence && *sequence < next_append_sequence
650 }
651 JournalInspectionCheckpoint::CheckingBatchIdentity {
652 sequence,
653 next_prior_sequence,
654 ..
655 } => {
656 *sequence > fold_sequence
657 && *sequence < next_append_sequence
658 && *next_prior_sequence > fold_sequence
659 && *next_prior_sequence < *sequence
660 }
661 JournalInspectionCheckpoint::AfterBatch { sequence } => {
662 *sequence >= fold_sequence && *sequence < next_append_sequence
663 }
664 }
665}
666
667fn strictly_sorted_unique(values: &[IntegrityVerifierFamily]) -> bool {
668 values.windows(2).all(|pair| pair[0] < pair[1])
669}
670
671#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
674pub enum IntegrityJobError {
675 CapacityExceeded,
677
678 CorruptProgressHeader,
680
681 CorruptProgressRecord,
683
684 CounterExhausted,
686
687 EntityIdentityMismatch,
689
690 IncompatibleProgressFormat,
692
693 Internal,
695
696 InvalidEntityIdentity,
698
699 InvalidJobId,
701
702 InvalidOwner,
704
705 InvalidSubmissionKey,
707
708 JobBusy,
710
711 JobIncarnationMismatch,
713
714 JobNotFound,
716
717 JobOwnerMismatch,
719
720 StaleAcknowledgement,
722
723 SubmissionAlreadyAdvanced,
725
726 SubmissionConflict,
728}
729
730#[derive(Debug)]
733pub enum IntegrityDeepError {
734 Internal(crate::error::InternalError),
736
737 Job(IntegrityJobError),
739}
740
741impl From<IntegrityJobError> for IntegrityDeepError {
742 fn from(error: IntegrityJobError) -> Self {
743 Self::Job(error)
744 }
745}
746
747impl From<crate::error::InternalError> for IntegrityDeepError {
748 fn from(error: crate::error::InternalError) -> Self {
749 Self::Internal(error)
750 }
751}
752
753#[cfg(test)]
754mod tests {
755 use super::*;
756
757 #[test]
758 fn public_job_inputs_revalidate_after_wire_decode() {
759 let owner_bytes =
760 candid::encode_one(IntegrityJobOwner(String::new())).expect("owner should encode");
761 let owner: IntegrityJobOwner =
762 candid::decode_one(&owner_bytes).expect("owner should decode");
763 assert_eq!(owner.validate(), Err(IntegrityJobError::InvalidOwner));
764
765 let submission_bytes = candid::encode_one(IntegritySubmissionKey(String::new()))
766 .expect("submission key should encode");
767 let submission: IntegritySubmissionKey =
768 candid::decode_one(&submission_bytes).expect("submission key should decode");
769 assert_eq!(
770 submission.validate(),
771 Err(IntegrityJobError::InvalidSubmissionKey),
772 );
773
774 let job_id_bytes =
775 candid::encode_one(IntegrityJobId([0; 32])).expect("job id should encode");
776 let job_id: IntegrityJobId =
777 candid::decode_one(&job_id_bytes).expect("job id should decode");
778 assert_eq!(job_id.validate(), Err(IntegrityJobError::InvalidJobId),);
779 }
780
781 #[test]
782 fn public_job_id_hex_round_trip_is_exact_and_fail_closed() {
783 let mut bytes = [0_u8; 32];
784 bytes[0] = 0x01;
785 bytes[31] = 0xfe;
786 let job_id = IntegrityJobId::try_from_bytes(bytes).expect("job id should admit");
787 let encoded = job_id.to_hex();
788
789 assert_eq!(encoded.len(), 64);
790 assert_eq!(IntegrityJobId::try_from_hex(encoded.as_str()), Ok(job_id),);
791 assert_eq!(
792 IntegrityJobId::try_from_hex(encoded.to_uppercase().as_str()),
793 Ok(job_id),
794 );
795 for malformed in [
796 "",
797 "01",
798 "0000000000000000000000000000000000000000000000000000000000000000",
799 "g001000000000000000000000000000000000000000000000000000000000000",
800 ] {
801 assert_eq!(
802 IntegrityJobId::try_from_hex(malformed),
803 Err(IntegrityJobError::InvalidJobId),
804 );
805 }
806 }
807
808 #[test]
809 fn persisted_checkpoint_families_stay_phase_owned() {
810 let journal_in_row = PhysicalUnitCheckpoint::Within {
811 physical_key: vec![1],
812 verifier_family: IntegrityVerifierFamily::JournalEnvelope,
813 ordinal: 0,
814 };
815 let row_in_index = PhysicalUnitCheckpoint::Within {
816 physical_key: vec![1],
817 verifier_family: IntegrityVerifierFamily::FieldValue,
818 ordinal: 0,
819 };
820 let reverse_in_reverse = PhysicalUnitCheckpoint::Within {
821 physical_key: vec![1],
822 verifier_family: IntegrityVerifierFamily::ReverseRelationEntry,
823 ordinal: 0,
824 };
825
826 assert!(!row_checkpoint_is_well_formed(&journal_in_row));
827 assert!(!index_checkpoint_is_well_formed(&row_in_index));
828 assert!(reverse_checkpoint_is_well_formed(&reverse_in_reverse));
829 }
830
831 #[test]
832 fn persisted_journal_checkpoint_cannot_skip_the_captured_tail_interval() {
833 assert!(journal_checkpoint_is_well_formed(
834 &JournalInspectionCheckpoint::BeforeFirst,
835 4,
836 8,
837 ));
838 assert!(journal_checkpoint_is_well_formed(
839 &JournalInspectionCheckpoint::BeforeBatch { sequence: 7 },
840 4,
841 8,
842 ));
843 assert!(journal_checkpoint_is_well_formed(
844 &JournalInspectionCheckpoint::AfterBatch { sequence: 4 },
845 4,
846 8,
847 ));
848 assert!(!journal_checkpoint_is_well_formed(
849 &JournalInspectionCheckpoint::BeforeBatch { sequence: 4 },
850 4,
851 8,
852 ));
853 assert!(!journal_checkpoint_is_well_formed(
854 &JournalInspectionCheckpoint::AfterBatch { sequence: 8 },
855 4,
856 8,
857 ));
858 assert!(!journal_checkpoint_is_well_formed(
859 &JournalInspectionCheckpoint::CheckingBatchIdentity {
860 sequence: 7,
861 batch_id: [1; 16],
862 next_prior_sequence: 4,
863 },
864 4,
865 8,
866 ));
867 }
868}