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, 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/// Opaque lookup identity for one retained Deep inspection job.
27
28#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
29pub struct IntegrityJobId([u8; 32]);
30
31impl IntegrityJobId {
32    /// Admit one nonzero current-form lookup identity.
33    ///
34    /// # Errors
35    ///
36    /// Returns [`IntegrityJobError::CorruptProgressRecord`] when `bytes` is
37    /// the reserved all-zero identity.
38    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    /// Decode one exact lowercase or uppercase hexadecimal job identity.
46    ///
47    /// # Errors
48    ///
49    /// Returns [`IntegrityJobError::InvalidJobId`] unless `value` contains
50    /// exactly 64 hexadecimal characters and decodes to a nonzero identity.
51    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    /// Return the canonical lookup bytes.
70    #[must_use]
71    pub const fn to_bytes(self) -> [u8; 32] {
72        self.0
73    }
74
75    /// Render the canonical lowercase hexadecimal SQL/shell identity.
76    #[must_use]
77    pub fn to_hex(self) -> String {
78        encode_hex_lower(&self.0)
79    }
80
81    /// Revalidate a possibly deserialized job identity before lookup.
82    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/// Bounded authorization identity persisted with one job.
100
101#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
102pub struct IntegrityJobOwner(String);
103
104impl IntegrityJobOwner {
105    /// Admit one nonempty bounded owner identity.
106    ///
107    /// # Errors
108    ///
109    /// Returns [`IntegrityJobError::InvalidOwner`] when the identity is empty
110    /// or exceeds the protocol bound.
111    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    /// Borrow the canonical owner identity.
120    #[must_use]
121    pub const fn as_str(&self) -> &str {
122        self.0.as_str()
123    }
124
125    /// Revalidate a possibly deserialized owner before authorization.
126    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/// Bounded client idempotency identity for Deep start.
135
136#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
137pub struct IntegritySubmissionKey(String);
138
139impl IntegritySubmissionKey {
140    /// Admit one nonempty bounded submission key.
141    ///
142    /// # Errors
143    ///
144    /// Returns [`IntegrityJobError::InvalidSubmissionKey`] when the key is
145    /// empty or exceeds the protocol bound.
146    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    /// Borrow the canonical submission key.
155    #[must_use]
156    pub const fn as_str(&self) -> &str {
157        self.0.as_str()
158    }
159
160    /// Revalidate a possibly deserialized key before job identity derivation.
161    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/// Exact private continuation for the current Deep phase.
170#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
171pub(in crate::db) enum IntegrityCheckpoint {
172    /// Bounded accepted metadata and control closure.
173    QuickMetadata,
174    /// Canonical source-row interval.
175    Rows(PhysicalUnitCheckpoint),
176    /// One active forward-index domain in accepted plan order.
177    Index {
178        ordinal: u32,
179        checkpoint: PhysicalUnitCheckpoint,
180    },
181    /// One active source-owned reverse domain in accepted plan order.
182    ReverseRelation {
183        ordinal: u32,
184        checkpoint: PhysicalUnitCheckpoint,
185    },
186    /// One participating journal tail in canonical store order.
187    Journal {
188        store_ordinal: u32,
189        checkpoint: JournalInspectionCheckpoint,
190    },
191    /// No physical traversal remains; only final proof equality may complete.
192    FinalProof,
193}
194
195impl IntegrityCheckpoint {
196    /// Return the canonical phase implied by this checkpoint.
197    #[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/// Frozen intent that supersedes advancement after the outstanding page is acknowledged.
211
212#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
213pub enum IntegrityPendingTerminal {
214    /// The inactivity lease elapsed.
215    Expired,
216    /// The authorized owner requested abort.
217    Aborted,
218}
219
220/// Stable terminal meaning of a completed Deep job.
221
222#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
223pub enum IntegrityTerminalOutcome {
224    /// Every phase exhausted cleanly under one unchanged proof.
225    DeepCompleteClean,
226    /// Every phase exhausted with one or more definite findings.
227    DeepCompleteWithFindings,
228    /// One proof component changed before completion.
229    Invalidated,
230    /// Accepted authority could not be inspected.
231    Uninspectable(IntegrityAuthorityDiagnostic),
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.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/// Typed Deep protocol or progress-record failure.
714
715#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
716pub enum IntegrityJobError {
717    /// The bounded progress store cannot admit another job.
718    CapacityExceeded,
719
720    /// The progress-store header is malformed.
721    CorruptProgressHeader,
722
723    /// The retained job record violates its current-form contract.
724    CorruptProgressRecord,
725
726    /// A checked protocol counter cannot advance.
727    CounterExhausted,
728
729    /// The accepted entity selector no longer matches runtime authority.
730    EntityIdentityMismatch,
731
732    /// The progress store uses an unsupported persisted format.
733    IncompatibleProgressFormat,
734
735    /// An internal Deep controller operation failed.
736    Internal,
737
738    /// The accepted entity selector is malformed.
739    InvalidEntityIdentity,
740
741    /// A caller-authored job identity is malformed or reserved.
742    InvalidJobId,
743
744    /// The authorization owner is empty or exceeds its bound.
745    InvalidOwner,
746
747    /// The idempotency key is empty or exceeds its bound.
748    InvalidSubmissionKey,
749
750    /// The job belongs to another database incarnation.
751    JobIncarnationMismatch,
752
753    /// No retained job has the supplied identity.
754    JobNotFound,
755
756    /// The supplied authorization owner does not own the job.
757    JobOwnerMismatch,
758
759    /// The acknowledgement does not name the outstanding receipt.
760    StaleAcknowledgement,
761
762    /// The Deep-start proof changed before a job could be published.
763    StartInvalidated,
764
765    /// A retried start names a job that already advanced.
766    SubmissionAlreadyAdvanced,
767
768    /// An owner/key pair was reused for a different target.
769    SubmissionConflict,
770}
771
772/// Deep protocol failures keep persisted-protocol and engine causes distinct.
773
774#[derive(Debug)]
775pub enum IntegrityDeepError {
776    /// Accepted authority or physical execution failed.
777    Internal(crate::error::InternalError),
778
779    /// Stable job/progress protocol rejected the request.
780    Job(IntegrityJobError),
781
782    /// Deep start could identify but not safely load accepted authority.
783    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}