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::ValidatedConstraints
650                    | IntegrityVerifierFamily::ForwardIndex
651                    | IntegrityVerifierFamily::Relation
652                    | IntegrityVerifierFamily::ReverseRelationEntry
653                    | IntegrityVerifierFamily::JournalEnvelope
654                    | IntegrityVerifierFamily::JournalBatchIdentity,
655                ..
656            }
657        )
658}
659
660fn reverse_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
661    checkpoint.raw_index_key().is_ok()
662        && !matches!(
663            checkpoint,
664            PhysicalUnitCheckpoint::Within {
665                verifier_family: IntegrityVerifierFamily::DataKey
666                    | IntegrityVerifierFamily::RowEnvelope
667                    | IntegrityVerifierFamily::FieldValue
668                    | IntegrityVerifierFamily::PrimaryKey
669                    | IntegrityVerifierFamily::ValidatedConstraints
670                    | IntegrityVerifierFamily::ForwardIndex
671                    | IntegrityVerifierFamily::Relation
672                    | IntegrityVerifierFamily::IndexEntry
673                    | IntegrityVerifierFamily::UniqueIndex
674                    | IntegrityVerifierFamily::JournalEnvelope
675                    | IntegrityVerifierFamily::JournalBatchIdentity,
676                ..
677            }
678        )
679}
680
681const fn journal_checkpoint_is_well_formed(
682    checkpoint: &JournalInspectionCheckpoint,
683    fold_sequence: u64,
684    next_append_sequence: u64,
685) -> bool {
686    match checkpoint {
687        JournalInspectionCheckpoint::BeforeFirst => true,
688        JournalInspectionCheckpoint::BeforeBatch { sequence } => {
689            *sequence > fold_sequence && *sequence < next_append_sequence
690        }
691        JournalInspectionCheckpoint::CheckingBatchIdentity {
692            sequence,
693            next_prior_sequence,
694            ..
695        } => {
696            *sequence > fold_sequence
697                && *sequence < next_append_sequence
698                && *next_prior_sequence > fold_sequence
699                && *next_prior_sequence < *sequence
700        }
701        JournalInspectionCheckpoint::AfterBatch { sequence } => {
702            *sequence >= fold_sequence && *sequence < next_append_sequence
703        }
704    }
705}
706
707fn strictly_sorted_unique(values: &[IntegrityVerifierFamily]) -> bool {
708    values.windows(2).all(|pair| pair[0] < pair[1])
709}
710
711/// Typed Deep protocol or progress-record failure.
712
713#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
714pub enum IntegrityJobError {
715    /// The bounded progress store cannot admit another job.
716    CapacityExceeded,
717
718    /// The progress-store header is malformed.
719    CorruptProgressHeader,
720
721    /// The retained job record violates its current-form contract.
722    CorruptProgressRecord,
723
724    /// A checked protocol counter cannot advance.
725    CounterExhausted,
726
727    /// The accepted entity selector no longer matches runtime authority.
728    EntityIdentityMismatch,
729
730    /// The progress store uses an unsupported persisted format.
731    IncompatibleProgressFormat,
732
733    /// An internal Deep controller operation failed.
734    Internal,
735
736    /// The accepted entity selector is malformed.
737    InvalidEntityIdentity,
738
739    /// A caller-authored job identity is malformed or reserved.
740    InvalidJobId,
741
742    /// The authorization owner is empty or exceeds its bound.
743    InvalidOwner,
744
745    /// The idempotency key is empty or exceeds its bound.
746    InvalidSubmissionKey,
747
748    /// The job belongs to another database incarnation.
749    JobIncarnationMismatch,
750
751    /// No retained job has the supplied identity.
752    JobNotFound,
753
754    /// The supplied authorization owner does not own the job.
755    JobOwnerMismatch,
756
757    /// The acknowledgement does not name the outstanding receipt.
758    StaleAcknowledgement,
759
760    /// The Deep-start proof changed before a job could be published.
761    StartInvalidated,
762
763    /// A retried start names a job that already advanced.
764    SubmissionAlreadyAdvanced,
765
766    /// An owner/key pair was reused for a different target.
767    SubmissionConflict,
768}
769
770/// Deep protocol failures keep persisted-protocol and engine causes distinct.
771
772#[derive(Debug)]
773pub enum IntegrityDeepError {
774    /// Accepted authority or physical execution failed.
775    Internal(crate::error::InternalError),
776
777    /// Stable job/progress protocol rejected the request.
778    Job(IntegrityJobError),
779
780    /// Deep start could identify but not safely load accepted authority.
781    Uninspectable(IntegrityAuthorityDiagnostic),
782}
783
784impl From<IntegrityJobError> for IntegrityDeepError {
785    fn from(error: IntegrityJobError) -> Self {
786        Self::Job(error)
787    }
788}
789
790impl From<crate::error::InternalError> for IntegrityDeepError {
791    fn from(error: crate::error::InternalError) -> Self {
792        Self::Internal(error)
793    }
794}
795
796#[cfg(test)]
797mod tests {
798    use super::*;
799
800    #[test]
801    fn public_job_inputs_revalidate_after_wire_decode() {
802        let owner_bytes =
803            candid::encode_one(IntegrityJobOwner(String::new())).expect("owner should encode");
804        let owner: IntegrityJobOwner =
805            candid::decode_one(&owner_bytes).expect("owner should decode");
806        assert_eq!(owner.validate(), Err(IntegrityJobError::InvalidOwner));
807
808        let submission_bytes = candid::encode_one(IntegritySubmissionKey(String::new()))
809            .expect("submission key should encode");
810        let submission: IntegritySubmissionKey =
811            candid::decode_one(&submission_bytes).expect("submission key should decode");
812        assert_eq!(
813            submission.validate(),
814            Err(IntegrityJobError::InvalidSubmissionKey),
815        );
816
817        let job_id_bytes =
818            candid::encode_one(IntegrityJobId([0; 32])).expect("job id should encode");
819        let job_id: IntegrityJobId =
820            candid::decode_one(&job_id_bytes).expect("job id should decode");
821        assert_eq!(job_id.validate(), Err(IntegrityJobError::InvalidJobId),);
822    }
823
824    #[test]
825    fn persisted_constraint_value_paths_require_current_accepted_id_shape() {
826        let valid = ConstraintValuePath::new(vec![
827            ConstraintValuePathComponent::RootField { field_id: 1 },
828            ConstraintValuePathComponent::RecordMember {
829                composite_type_id: 2,
830                member_id: 3,
831            },
832            ConstraintValuePathComponent::ListElement { index: 0 },
833        ]);
834        assert!(constraint_value_path_is_well_formed(&valid));
835
836        for malformed in [
837            ConstraintValuePath::new(Vec::new()),
838            ConstraintValuePath::new(vec![ConstraintValuePathComponent::RootField {
839                field_id: 0,
840            }]),
841            ConstraintValuePath::new(vec![
842                ConstraintValuePathComponent::RootField { field_id: 1 },
843                ConstraintValuePathComponent::RootField { field_id: 2 },
844            ]),
845            ConstraintValuePath::new(vec![
846                ConstraintValuePathComponent::RootField { field_id: 1 },
847                ConstraintValuePathComponent::Newtype {
848                    composite_type_id: 0,
849                },
850            ]),
851        ] {
852            assert!(!constraint_value_path_is_well_formed(&malformed));
853        }
854    }
855
856    #[test]
857    fn public_job_id_hex_round_trip_is_exact_and_fail_closed() {
858        let mut bytes = [0_u8; 32];
859        bytes[0] = 0x01;
860        bytes[31] = 0xfe;
861        let job_id = IntegrityJobId::try_from_bytes(bytes).expect("job id should admit");
862        let encoded = job_id.to_hex();
863
864        assert_eq!(encoded.len(), 64);
865        assert_eq!(IntegrityJobId::try_from_hex(encoded.as_str()), Ok(job_id),);
866        assert_eq!(
867            IntegrityJobId::try_from_hex(encoded.to_uppercase().as_str()),
868            Ok(job_id),
869        );
870        for malformed in [
871            "",
872            "01",
873            "0000000000000000000000000000000000000000000000000000000000000000",
874            "g001000000000000000000000000000000000000000000000000000000000000",
875        ] {
876            assert_eq!(
877                IntegrityJobId::try_from_hex(malformed),
878                Err(IntegrityJobError::InvalidJobId),
879            );
880        }
881    }
882
883    #[test]
884    fn persisted_checkpoint_families_stay_phase_owned() {
885        let journal_in_row = PhysicalUnitCheckpoint::Within {
886            physical_key: vec![1],
887            verifier_family: IntegrityVerifierFamily::JournalEnvelope,
888            ordinal: 0,
889        };
890        let row_in_index = PhysicalUnitCheckpoint::Within {
891            physical_key: vec![1],
892            verifier_family: IntegrityVerifierFamily::FieldValue,
893            ordinal: 0,
894        };
895        let reverse_in_reverse = PhysicalUnitCheckpoint::Within {
896            physical_key: vec![1],
897            verifier_family: IntegrityVerifierFamily::ReverseRelationEntry,
898            ordinal: 0,
899        };
900
901        assert!(!row_checkpoint_is_well_formed(&journal_in_row));
902        assert!(!index_checkpoint_is_well_formed(&row_in_index));
903        assert!(reverse_checkpoint_is_well_formed(&reverse_in_reverse));
904    }
905
906    #[test]
907    fn persisted_journal_checkpoint_cannot_skip_the_captured_tail_interval() {
908        assert!(journal_checkpoint_is_well_formed(
909            &JournalInspectionCheckpoint::BeforeFirst,
910            4,
911            8,
912        ));
913        assert!(journal_checkpoint_is_well_formed(
914            &JournalInspectionCheckpoint::BeforeBatch { sequence: 7 },
915            4,
916            8,
917        ));
918        assert!(journal_checkpoint_is_well_formed(
919            &JournalInspectionCheckpoint::AfterBatch { sequence: 4 },
920            4,
921            8,
922        ));
923        assert!(!journal_checkpoint_is_well_formed(
924            &JournalInspectionCheckpoint::BeforeBatch { sequence: 4 },
925            4,
926            8,
927        ));
928        assert!(!journal_checkpoint_is_well_formed(
929            &JournalInspectionCheckpoint::AfterBatch { sequence: 8 },
930            4,
931            8,
932        ));
933        assert!(!journal_checkpoint_is_well_formed(
934            &JournalInspectionCheckpoint::CheckingBatchIdentity {
935                sequence: 7,
936                batch_id: [1; 16],
937                next_prior_sequence: 4,
938            },
939            4,
940            8,
941        ));
942    }
943}