Skip to main content

icydb_core/db/integrity/
job.rs

1//! Module: db::integrity::job
2//! Responsibility: invariant-bearing Deep job, checkpoint, and receipt vocabulary.
3//! Does not own: stable storage, physical traversal, authorization, or time.
4//! Boundary: Deep controller <-> current-form progress record codec.
5
6use 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/// Opaque lookup identity for one retained Deep inspection job.
25
26#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
27pub struct IntegrityJobId([u8; 32]);
28
29impl IntegrityJobId {
30    /// Admit one nonzero current-form lookup identity.
31    ///
32    /// # Errors
33    ///
34    /// Returns [`IntegrityJobError::CorruptProgressRecord`] when `bytes` is
35    /// the reserved all-zero identity.
36    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    /// Decode one exact lowercase or uppercase hexadecimal job identity.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`IntegrityJobError::InvalidJobId`] unless `value` contains
48    /// exactly 64 hexadecimal characters and decodes to a nonzero identity.
49    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    /// Return the canonical lookup bytes.
68    #[must_use]
69    pub const fn to_bytes(self) -> [u8; 32] {
70        self.0
71    }
72
73    /// Render the canonical lowercase hexadecimal SQL/shell identity.
74    #[must_use]
75    pub fn to_hex(self) -> String {
76        encode_hex_lower(&self.0)
77    }
78
79    /// Revalidate a possibly deserialized job identity before lookup.
80    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/// Bounded authorization identity persisted with one job.
98
99#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
100pub struct IntegrityJobOwner(String);
101
102impl IntegrityJobOwner {
103    /// Admit one nonempty bounded owner identity.
104    ///
105    /// # Errors
106    ///
107    /// Returns [`IntegrityJobError::InvalidOwner`] when the identity is empty
108    /// or exceeds the protocol bound.
109    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    /// Borrow the canonical owner identity.
118    #[must_use]
119    pub const fn as_str(&self) -> &str {
120        self.0.as_str()
121    }
122
123    /// Revalidate a possibly deserialized owner before authorization.
124    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/// Bounded client idempotency identity for Deep start.
133
134#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
135pub struct IntegritySubmissionKey(String);
136
137impl IntegritySubmissionKey {
138    /// Admit one nonempty bounded submission key.
139    ///
140    /// # Errors
141    ///
142    /// Returns [`IntegrityJobError::InvalidSubmissionKey`] when the key is
143    /// empty or exceeds the protocol bound.
144    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    /// Borrow the canonical submission key.
153    #[must_use]
154    pub const fn as_str(&self) -> &str {
155        self.0.as_str()
156    }
157
158    /// Revalidate a possibly deserialized key before job identity derivation.
159    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/// Exact private continuation for the current Deep phase.
168#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
169pub(in crate::db) enum IntegrityCheckpoint {
170    /// Bounded accepted metadata and control closure.
171    QuickMetadata,
172    /// Canonical source-row interval.
173    Rows(PhysicalUnitCheckpoint),
174    /// One active forward-index domain in accepted plan order.
175    Index {
176        ordinal: u32,
177        checkpoint: PhysicalUnitCheckpoint,
178    },
179    /// One active source-owned reverse domain in accepted plan order.
180    ReverseRelation {
181        ordinal: u32,
182        checkpoint: PhysicalUnitCheckpoint,
183    },
184    /// One participating journal tail in canonical store order.
185    Journal {
186        store_ordinal: u32,
187        checkpoint: JournalInspectionCheckpoint,
188    },
189    /// No physical traversal remains; only final proof equality may complete.
190    FinalProof,
191}
192
193impl IntegrityCheckpoint {
194    /// Return the canonical phase implied by this checkpoint.
195    #[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/// Frozen intent that supersedes advancement after the outstanding page is acknowledged.
209
210#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
211pub enum IntegrityPendingTerminal {
212    /// The inactivity lease elapsed.
213    Expired,
214    /// The authorized owner requested abort.
215    Aborted,
216}
217
218/// Stable terminal meaning of a completed Deep job.
219
220#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
221pub enum IntegrityTerminalOutcome {
222    /// Every phase exhausted cleanly under one unchanged proof.
223    DeepCompleteClean,
224    /// Every phase exhausted with one or more definite findings.
225    DeepCompleteWithFindings,
226    /// One proof component changed before completion.
227    Invalidated,
228    /// Accepted authority could not be inspected.
229    Uninspectable(IntegrityAuthorityDiagnostic),
230    /// A load-bearing physical traversal could not prove progress.
231    UninspectableStorage(StorageTraversalCorruption),
232    /// One frozen bounded resource was insufficient.
233    ResourceLimited(IntegrityResourceDiagnostic),
234    /// The inactivity lease expired.
235    Expired,
236    /// The authorized owner aborted the job.
237    Aborted,
238}
239
240/// Durable job lifecycle state.
241#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
242pub(in crate::db) enum IntegrityJobState {
243    /// Physical advancement remains permitted.
244    InProgress,
245    /// Advancement is frozen while the last page remains unacknowledged.
246    TerminalPending(IntegrityPendingTerminal),
247    /// One final receipt is retained for replay and acknowledgement.
248    Terminal {
249        outcome: IntegrityTerminalOutcome,
250        receipt_acknowledged: bool,
251    },
252}
253
254/// Semantic status carried by one bounded Deep page.
255
256#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
257pub enum DeepIntegrityPageStatus {
258    /// More physical work or the final proof check remains.
259    InProgress,
260    /// This receipt records the stable terminal result.
261    Terminal(IntegrityTerminalOutcome),
262}
263
264/// One bounded replayable Deep result page.
265
266#[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    /// Return the opaque job lookup identity.
280    #[must_use]
281    pub const fn job_id(&self) -> IntegrityJobId {
282        self.job_id
283    }
284
285    /// Return the monotonically increasing receipt sequence.
286    #[must_use]
287    pub const fn page_sequence(&self) -> u64 {
288        self.page_sequence
289    }
290
291    /// Return the phase represented by this receipt.
292    #[must_use]
293    pub const fn phase(&self) -> IntegrityPhase {
294        self.phase
295    }
296
297    /// Borrow the current or terminal status.
298    #[must_use]
299    pub const fn status(&self) -> &DeepIntegrityPageStatus {
300        &self.status
301    }
302
303    /// Return cumulative successfully persisted page count.
304    #[must_use]
305    pub const fn pages_completed(&self) -> u64 {
306        self.pages_completed
307    }
308
309    /// Return cumulative definite findings.
310    #[must_use]
311    pub const fn findings_seen(&self) -> u64 {
312        self.findings_seen
313    }
314
315    /// Borrow findings produced only by this page.
316    #[must_use]
317    pub const fn findings(&self) -> &[IntegrityFinding] {
318        self.findings.as_slice()
319    }
320
321    /// Borrow the cumulative canonical blocked-family set.
322    #[must_use]
323    pub const fn blocked_verifier_families(&self) -> &[IntegrityVerifierFamily] {
324        self.blocked_verifier_families.as_slice()
325    }
326}
327
328/// Abort receipt status.
329
330#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
331pub enum IntegrityAbortStatus {
332    /// Abort is frozen but cannot replace the outstanding page yet.
333    TerminationPending(IntegrityPendingTerminal),
334    /// The terminal abort result is replayable.
335    Terminal(IntegrityTerminalOutcome),
336}
337
338/// One bounded abort/expiry receipt.
339
340#[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    /// Return the opaque job identity.
349    #[must_use]
350    pub const fn job_id(&self) -> IntegrityJobId {
351        self.job_id
352    }
353
354    /// Return the outstanding or terminal receipt sequence.
355    #[must_use]
356    pub const fn page_sequence(&self) -> u64 {
357        self.page_sequence
358    }
359
360    /// Borrow the pending or terminal abort status.
361    #[must_use]
362    pub const fn status(&self) -> &IntegrityAbortStatus {
363        &self.status
364    }
365}
366
367/// Persisted receipt body.
368
369#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
370pub enum IntegrityJobReceipt {
371    /// Normal start, advancement, or completion page.
372    Page(DeepIntegrityPage),
373    /// Abort/expiry terminal or pending acknowledgement.
374    Abort(IntegrityAbortReceipt),
375}
376
377impl IntegrityJobReceipt {
378    /// Return the job identity carried by this receipt.
379    #[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    /// Return the sequence carried by this receipt.
388    #[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/// Request identity that produced the cached receipt.
398#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
399pub(in crate::db) enum IntegrityReceiptReplayKey {
400    /// Initial Deep start.
401    Start,
402    /// Continue request acknowledging the named prior sequence.
403    Continue { acknowledged_sequence: u64 },
404}
405
406/// One cached bounded receipt and its exact replay request.
407#[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/// Current invariant-bearing durable Deep record.
414#[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    /// Validate all persisted cross-field invariants before use.
436    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/// Typed Deep protocol or progress-record failure.
672
673#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
674pub enum IntegrityJobError {
675    /// The bounded progress store cannot admit another job.
676    CapacityExceeded,
677
678    /// The progress-store header is malformed.
679    CorruptProgressHeader,
680
681    /// The retained job record violates its current-form contract.
682    CorruptProgressRecord,
683
684    /// A checked protocol counter cannot advance.
685    CounterExhausted,
686
687    /// The accepted entity selector no longer matches runtime authority.
688    EntityIdentityMismatch,
689
690    /// The progress store uses an unsupported persisted format.
691    IncompatibleProgressFormat,
692
693    /// An internal Deep controller operation failed.
694    Internal,
695
696    /// The accepted entity selector is malformed.
697    InvalidEntityIdentity,
698
699    /// A caller-authored job identity is malformed or reserved.
700    InvalidJobId,
701
702    /// The authorization owner is empty or exceeds its bound.
703    InvalidOwner,
704
705    /// The idempotency key is empty or exceeds its bound.
706    InvalidSubmissionKey,
707
708    /// Another bounded advancement currently owns the job.
709    JobBusy,
710
711    /// The job belongs to another database incarnation.
712    JobIncarnationMismatch,
713
714    /// No retained job has the supplied identity.
715    JobNotFound,
716
717    /// The supplied authorization owner does not own the job.
718    JobOwnerMismatch,
719
720    /// The acknowledgement does not name the outstanding receipt.
721    StaleAcknowledgement,
722
723    /// A retried start names a job that already advanced.
724    SubmissionAlreadyAdvanced,
725
726    /// An owner/key pair was reused for a different target.
727    SubmissionConflict,
728}
729
730/// Deep protocol failures keep persisted-protocol and engine causes distinct.
731
732#[derive(Debug)]
733pub enum IntegrityDeepError {
734    /// Accepted authority or physical execution failed.
735    Internal(crate::error::InternalError),
736
737    /// Stable job/progress protocol rejected the request.
738    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}