Skip to main content

chio_kernel/
dispatch_status.rs

1//! Closed verification boundary for provider-side dispatch recovery.
2//!
3//! Provider DTOs are untrusted until this module validates a monotonic attempt
4//! chain and any referenced bytes. With no internally qualified provider, the
5//! only result is `Unknown`; no generic tool transport is qualified by default.
6
7use std::{fmt, sync::Arc};
8
9#[cfg(test)]
10use chio_core_types::canonical_json_bytes;
11use chio_core_types::provider_attempt::{
12    validate_provider_checkpoint_chain, ProviderAcceptanceBindingV1, ProviderAttemptBindingV1,
13    ProviderAttemptCheckpointV1, ProviderAttemptPhaseV1, ProviderCancellationBindingV1,
14    ProviderCompletionBindingV1, ProviderExecutionLeaseBindingV1,
15};
16use chio_core_types::sha256_hex;
17#[cfg(test)]
18use serde::Serialize;
19
20#[cfg(test)]
21const PROVIDER_QUALIFICATION_DOMAIN: &str = "chio.dispatch-status-provider-qualification.v1";
22const I_JSON_MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct DispatchStatusProviderError {
26    message: String,
27}
28
29impl DispatchStatusProviderError {
30    #[must_use]
31    pub fn new(message: impl Into<String>) -> Self {
32        Self {
33            message: message.into(),
34        }
35    }
36}
37
38impl fmt::Display for DispatchStatusProviderError {
39    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40        formatter.write_str(&self.message)
41    }
42}
43
44impl std::error::Error for DispatchStatusProviderError {}
45
46#[derive(Debug, thiserror::Error)]
47pub enum DispatchStatusError {
48    #[error("invalid dispatch status query: {0}")]
49    InvalidQuery(String),
50    #[error("invalid provider qualification: {0}")]
51    InvalidQualification(String),
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct DispatchStatusQuery {
56    pub attempt: ProviderAttemptBindingV1,
57    /// Untrusted continuity hint. This is never promoted to provider evidence.
58    pub last_checkpoint: Option<ProviderAttemptCheckpointV1>,
59    pub observed_at: u64,
60}
61
62impl DispatchStatusQuery {
63    pub fn validate(&self) -> Result<(), DispatchStatusError> {
64        self.attempt
65            .validate()
66            .map_err(|error| DispatchStatusError::InvalidQuery(error.to_string()))?;
67        if self.observed_at == 0 || self.observed_at > I_JSON_MAX_SAFE_INTEGER {
68            return Err(DispatchStatusError::InvalidQuery(
69                "observed_at must be a positive I-JSON safe integer".to_string(),
70            ));
71        }
72        if let Some(checkpoint) = &self.last_checkpoint {
73            checkpoint
74                .validate()
75                .map_err(|error| DispatchStatusError::InvalidQuery(error.to_string()))?;
76            if checkpoint.attempt != self.attempt {
77                return Err(DispatchStatusError::InvalidQuery(
78                    "last checkpoint is bound to another provider attempt".to_string(),
79                ));
80            }
81        }
82        Ok(())
83    }
84}
85
86/// An observation claimed by a provider adapter.
87///
88/// A checkpoint response contains every checkpoint strictly after the query's
89/// `last_checkpoint`, or the full chain starting at genesis when no local
90/// checkpoint exists. An empty response contains no verifiable provider state.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum ProviderDispatchStatusObservation {
93    Checkpoints(Vec<ProviderAttemptCheckpointV1>),
94    Unknown,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct AuthenticatedProviderAcceptance {
99    pub binding: ProviderAcceptanceBindingV1,
100    pub envelope: Vec<u8>,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct AuthenticatedProviderNotAccepted {
105    pub binding: ProviderCancellationBindingV1,
106    pub proof: Vec<u8>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct AuthenticatedProviderCompletedOutcome {
111    pub binding: ProviderCompletionBindingV1,
112    pub outcome_bytes: Vec<u8>,
113    pub cost_units: u64,
114    pub currency: String,
115    pub terminal_evidence: Vec<u8>,
116}
117
118/// Untrusted provider transport surface.
119///
120/// Implementing this trait does not qualify an adapter. Only the private,
121/// provider-owning qualification boundary below can make these responses
122/// eligible for verification.
123pub trait DispatchStatusProvider: Send + Sync {
124    fn transport_id(&self) -> &str;
125    fn transport_key_epoch(&self) -> u64;
126
127    fn status(
128        &self,
129        query: &DispatchStatusQuery,
130    ) -> Result<ProviderDispatchStatusObservation, DispatchStatusProviderError>;
131
132    fn fetch_acceptance(
133        &self,
134        binding: &ProviderAcceptanceBindingV1,
135    ) -> Result<AuthenticatedProviderAcceptance, DispatchStatusProviderError>;
136
137    fn fetch_not_accepted(
138        &self,
139        binding: &ProviderCancellationBindingV1,
140    ) -> Result<AuthenticatedProviderNotAccepted, DispatchStatusProviderError>;
141
142    /// Fetch the exact authenticated bytes, cost, currency, and terminal
143    /// evidence bound by a completed checkpoint. A reference alone is never a
144    /// completed result.
145    fn fetch_completed_outcome(
146        &self,
147        binding: &ProviderCompletionBindingV1,
148    ) -> Result<AuthenticatedProviderCompletedOutcome, DispatchStatusProviderError>;
149}
150
151/// Sealed composition point for a concrete qualified adapter.
152///
153/// An implementation belongs in this module only after its constructor pins a
154/// transport key epoch and signed-message domain, owns the corresponding
155/// signature verifier, and reads a rollback-independent continuity anchor and
156/// high-water mark. A public `DispatchStatusProvider` implementation alone is
157/// deliberately insufficient.
158trait QualifiedDispatchStatusAdapter: DispatchStatusProvider {}
159
160/// Opaque authority to query one qualified provider instance.
161///
162/// The token owns the adapter it qualifies, is not deserializable, and has no
163/// public constructor. This prevents substituting a self-asserted provider with
164/// the same transport id and epoch.
165///
166/// ```compile_fail
167/// let _: Result<chio_kernel::dispatch_status::QualifiedDispatchStatusProvider, _> =
168///     serde_json::from_str("{}");
169/// ```
170pub struct QualifiedDispatchStatusProvider {
171    provider: Arc<dyn QualifiedDispatchStatusAdapter>,
172    transport_id: String,
173    transport_key_epoch: u64,
174    qualification_digest: String,
175}
176
177impl fmt::Debug for QualifiedDispatchStatusProvider {
178    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179        formatter
180            .debug_struct("QualifiedDispatchStatusProvider")
181            .field("transport_id", &self.transport_id)
182            .field("transport_key_epoch", &self.transport_key_epoch)
183            .field("qualification_digest", &self.qualification_digest)
184            .finish_non_exhaustive()
185    }
186}
187
188impl QualifiedDispatchStatusProvider {
189    #[must_use]
190    pub fn transport_id(&self) -> &str {
191        &self.transport_id
192    }
193
194    #[must_use]
195    pub const fn transport_key_epoch(&self) -> u64 {
196        self.transport_key_epoch
197    }
198
199    #[must_use]
200    pub fn qualification_digest(&self) -> &str {
201        &self.qualification_digest
202    }
203}
204
205#[cfg(test)]
206#[derive(Serialize)]
207struct TestQualificationBinding<'a> {
208    transport_id: &'a str,
209    transport_key_epoch: u64,
210    signature_verifier_id: &'a str,
211    continuity_anchor_id: &'a str,
212    continuity_high_water: u64,
213    signed_message_domain: &'a str,
214}
215
216#[cfg(test)]
217struct TestQualifiedDispatchStatusAdapter(Arc<dyn DispatchStatusProvider>);
218
219#[cfg(test)]
220impl DispatchStatusProvider for TestQualifiedDispatchStatusAdapter {
221    fn transport_id(&self) -> &str {
222        self.0.transport_id()
223    }
224
225    fn transport_key_epoch(&self) -> u64 {
226        self.0.transport_key_epoch()
227    }
228
229    fn status(
230        &self,
231        query: &DispatchStatusQuery,
232    ) -> Result<ProviderDispatchStatusObservation, DispatchStatusProviderError> {
233        self.0.status(query)
234    }
235
236    fn fetch_acceptance(
237        &self,
238        binding: &ProviderAcceptanceBindingV1,
239    ) -> Result<AuthenticatedProviderAcceptance, DispatchStatusProviderError> {
240        self.0.fetch_acceptance(binding)
241    }
242
243    fn fetch_not_accepted(
244        &self,
245        binding: &ProviderCancellationBindingV1,
246    ) -> Result<AuthenticatedProviderNotAccepted, DispatchStatusProviderError> {
247        self.0.fetch_not_accepted(binding)
248    }
249
250    fn fetch_completed_outcome(
251        &self,
252        binding: &ProviderCompletionBindingV1,
253    ) -> Result<AuthenticatedProviderCompletedOutcome, DispatchStatusProviderError> {
254        self.0.fetch_completed_outcome(binding)
255    }
256}
257
258#[cfg(test)]
259impl QualifiedDispatchStatusAdapter for TestQualifiedDispatchStatusAdapter {}
260
261#[cfg(test)]
262pub(crate) fn qualify_dispatch_status_provider_for_test(
263    provider: Arc<dyn DispatchStatusProvider>,
264    signature_verifier_id: &str,
265    continuity_anchor_id: &str,
266    continuity_high_water: u64,
267    signed_message_domain: &str,
268) -> Result<QualifiedDispatchStatusProvider, DispatchStatusError> {
269    qualify_sealed_dispatch_status_provider_for_test(
270        Arc::new(TestQualifiedDispatchStatusAdapter(provider)),
271        signature_verifier_id,
272        continuity_anchor_id,
273        continuity_high_water,
274        signed_message_domain,
275    )
276}
277
278#[cfg(test)]
279fn qualify_sealed_dispatch_status_provider_for_test(
280    provider: Arc<dyn QualifiedDispatchStatusAdapter>,
281    signature_verifier_id: &str,
282    continuity_anchor_id: &str,
283    continuity_high_water: u64,
284    signed_message_domain: &str,
285) -> Result<QualifiedDispatchStatusProvider, DispatchStatusError> {
286    let transport_id = provider.transport_id().to_string();
287    let transport_key_epoch = provider.transport_key_epoch();
288    validate_transport_binding(&transport_id, transport_key_epoch)?;
289    if !valid_qualification_id(signature_verifier_id)
290        || !valid_qualification_id(continuity_anchor_id)
291        || continuity_high_water == 0
292        || continuity_high_water > I_JSON_MAX_SAFE_INTEGER
293        || !valid_qualification_id(signed_message_domain)
294    {
295        return Err(DispatchStatusError::InvalidQualification(
296            "test qualification binding is invalid".to_string(),
297        ));
298    }
299    let canonical = canonical_json_bytes(&TestQualificationBinding {
300        transport_id: &transport_id,
301        transport_key_epoch,
302        signature_verifier_id,
303        continuity_anchor_id,
304        continuity_high_water,
305        signed_message_domain,
306    })
307    .map_err(|error| DispatchStatusError::InvalidQualification(error.to_string()))?;
308    let mut bytes = Vec::with_capacity(PROVIDER_QUALIFICATION_DOMAIN.len() + 1 + canonical.len());
309    bytes.extend_from_slice(PROVIDER_QUALIFICATION_DOMAIN.as_bytes());
310    bytes.push(0);
311    bytes.extend_from_slice(&canonical);
312    Ok(QualifiedDispatchStatusProvider {
313        provider,
314        transport_id,
315        transport_key_epoch,
316        qualification_digest: sha256_hex(&bytes),
317    })
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
321pub enum DispatchUnknownReason {
322    UnqualifiedProvider,
323    QualificationMismatch,
324    ProviderUnavailable,
325    ProviderReportedUnknown,
326    InvalidProviderEvidence,
327    AcceptanceUnavailable,
328    NotAcceptedProofUnavailable,
329    CompletedOutcomeUnavailable,
330    StaleExecutionLease,
331}
332
333#[derive(Debug, Clone, PartialEq, Eq)]
334pub struct VerifiedProviderPending {
335    qualification_digest: String,
336    checkpoint: ProviderAttemptCheckpointV1,
337    checkpoint_digest: String,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq)]
341pub struct VerifiedProviderNotAccepted {
342    qualification_digest: String,
343    checkpoint: ProviderAttemptCheckpointV1,
344    checkpoint_digest: String,
345    cancellation: ProviderCancellationBindingV1,
346    proof: Vec<u8>,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct VerifiedProviderAccepted {
351    qualification_digest: String,
352    checkpoint: ProviderAttemptCheckpointV1,
353    checkpoint_digest: String,
354    acceptance: ProviderAcceptanceBindingV1,
355    acceptance_envelope: Vec<u8>,
356    execution_lease: Option<ProviderExecutionLeaseBindingV1>,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub struct VerifiedProviderCompleted {
361    qualification_digest: String,
362    checkpoint: ProviderAttemptCheckpointV1,
363    checkpoint_digest: String,
364    completion: ProviderCompletionBindingV1,
365    outcome_bytes: Vec<u8>,
366    cost_units: u64,
367    currency: String,
368    terminal_evidence: Vec<u8>,
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
372pub struct VerifiedProviderUnknown {
373    attempt: ProviderAttemptBindingV1,
374    reason: DispatchUnknownReason,
375    last_checkpoint_digest: Option<String>,
376}
377
378#[derive(Debug, Clone, PartialEq, Eq)]
379pub enum VerifiedDispatchStatus {
380    NotAccepted(VerifiedProviderNotAccepted),
381    Pending(VerifiedProviderPending),
382    Accepted(VerifiedProviderAccepted),
383    Completed(VerifiedProviderCompleted),
384    Unknown(VerifiedProviderUnknown),
385}
386
387macro_rules! checkpoint_accessors {
388    ($type:ty) => {
389        impl $type {
390            #[must_use]
391            pub fn qualification_digest(&self) -> &str {
392                &self.qualification_digest
393            }
394
395            #[must_use]
396            pub fn checkpoint(&self) -> &ProviderAttemptCheckpointV1 {
397                &self.checkpoint
398            }
399
400            #[must_use]
401            pub fn checkpoint_digest(&self) -> &str {
402                &self.checkpoint_digest
403            }
404        }
405    };
406}
407
408checkpoint_accessors!(VerifiedProviderPending);
409checkpoint_accessors!(VerifiedProviderNotAccepted);
410checkpoint_accessors!(VerifiedProviderAccepted);
411checkpoint_accessors!(VerifiedProviderCompleted);
412
413impl VerifiedProviderNotAccepted {
414    #[must_use]
415    pub fn cancellation(&self) -> &ProviderCancellationBindingV1 {
416        &self.cancellation
417    }
418
419    #[must_use]
420    pub fn proof(&self) -> &[u8] {
421        &self.proof
422    }
423
424    #[cfg(test)]
425    pub(crate) fn with_cancellation_for_test(
426        &self,
427        cancellation: ProviderCancellationBindingV1,
428    ) -> Self {
429        Self {
430            cancellation,
431            ..self.clone()
432        }
433    }
434}
435
436impl VerifiedProviderAccepted {
437    #[must_use]
438    pub fn acceptance(&self) -> &ProviderAcceptanceBindingV1 {
439        &self.acceptance
440    }
441
442    #[must_use]
443    pub fn acceptance_envelope(&self) -> &[u8] {
444        &self.acceptance_envelope
445    }
446
447    #[must_use]
448    pub fn execution_lease(&self) -> Option<&ProviderExecutionLeaseBindingV1> {
449        self.execution_lease.as_ref()
450    }
451}
452
453impl VerifiedProviderCompleted {
454    #[must_use]
455    pub fn completion(&self) -> &ProviderCompletionBindingV1 {
456        &self.completion
457    }
458
459    #[must_use]
460    pub fn outcome_bytes(&self) -> &[u8] {
461        &self.outcome_bytes
462    }
463
464    #[must_use]
465    pub const fn cost_units(&self) -> u64 {
466        self.cost_units
467    }
468
469    #[must_use]
470    pub fn currency(&self) -> &str {
471        &self.currency
472    }
473
474    #[must_use]
475    pub fn terminal_evidence(&self) -> &[u8] {
476        &self.terminal_evidence
477    }
478}
479
480impl VerifiedProviderUnknown {
481    #[must_use]
482    pub fn attempt(&self) -> &ProviderAttemptBindingV1 {
483        &self.attempt
484    }
485
486    #[must_use]
487    pub const fn reason(&self) -> DispatchUnknownReason {
488        self.reason
489    }
490
491    #[must_use]
492    pub fn last_checkpoint_digest(&self) -> Option<&str> {
493        self.last_checkpoint_digest.as_deref()
494    }
495}
496
497pub fn resolve_dispatch_status(
498    qualification: Option<&QualifiedDispatchStatusProvider>,
499    query: &DispatchStatusQuery,
500) -> Result<VerifiedDispatchStatus, DispatchStatusError> {
501    query.validate()?;
502    let Some(qualification) = qualification else {
503        return Ok(unknown(query, DispatchUnknownReason::UnqualifiedProvider));
504    };
505    let provider = qualification.provider.as_ref();
506    if qualification.transport_id != query.attempt.transport_id
507        || qualification.transport_key_epoch != query.attempt.transport_key_epoch
508        || provider.transport_id() != query.attempt.transport_id
509        || provider.transport_key_epoch() != query.attempt.transport_key_epoch
510    {
511        return Ok(unknown(query, DispatchUnknownReason::QualificationMismatch));
512    }
513
514    let observation = match provider.status(query) {
515        Ok(observation) => observation,
516        Err(_) => return Ok(unknown(query, DispatchUnknownReason::ProviderUnavailable)),
517    };
518    let ProviderDispatchStatusObservation::Checkpoints(checkpoints) = observation else {
519        return Ok(unknown(
520            query,
521            DispatchUnknownReason::ProviderReportedUnknown,
522        ));
523    };
524    if validate_provider_checkpoint_chain(query.last_checkpoint.as_ref(), &checkpoints).is_err() {
525        return Ok(unknown(
526            query,
527            DispatchUnknownReason::InvalidProviderEvidence,
528        ));
529    }
530    let Some(checkpoint) = checkpoints.last().cloned() else {
531        return Ok(unknown(
532            query,
533            DispatchUnknownReason::InvalidProviderEvidence,
534        ));
535    };
536    if checkpoint.attempt != query.attempt {
537        return Ok(unknown(
538            query,
539            DispatchUnknownReason::InvalidProviderEvidence,
540        ));
541    }
542    let Ok(checkpoint_digest) = checkpoint.digest() else {
543        return Ok(unknown(
544            query,
545            DispatchUnknownReason::InvalidProviderEvidence,
546        ));
547    };
548
549    let phase = checkpoint.phase.clone();
550    if phase_observed_after(&phase, query.observed_at) {
551        return Ok(unknown(
552            query,
553            DispatchUnknownReason::InvalidProviderEvidence,
554        ));
555    }
556    match phase {
557        ProviderAttemptPhaseV1::Pending { .. } => {
558            Ok(VerifiedDispatchStatus::Pending(VerifiedProviderPending {
559                qualification_digest: qualification.qualification_digest.clone(),
560                checkpoint,
561                checkpoint_digest,
562            }))
563        }
564        ProviderAttemptPhaseV1::Cancelled { cancellation, .. } => {
565            let fetched = match provider.fetch_not_accepted(&cancellation) {
566                Ok(fetched) => fetched,
567                Err(_) => {
568                    return Ok(unknown(
569                        query,
570                        DispatchUnknownReason::NotAcceptedProofUnavailable,
571                    ));
572                }
573            };
574            if fetched.binding != cancellation
575                || !bytes_match(
576                    &fetched.proof,
577                    cancellation.no_acceptance_proof_size_bytes,
578                    &cancellation.no_acceptance_proof_sha256,
579                )
580            {
581                return Ok(unknown(
582                    query,
583                    DispatchUnknownReason::InvalidProviderEvidence,
584                ));
585            }
586            Ok(VerifiedDispatchStatus::NotAccepted(
587                VerifiedProviderNotAccepted {
588                    qualification_digest: qualification.qualification_digest.clone(),
589                    checkpoint,
590                    checkpoint_digest,
591                    cancellation,
592                    proof: fetched.proof,
593                },
594            ))
595        }
596        ProviderAttemptPhaseV1::Accepted { acceptance, .. } => verified_accepted(
597            provider,
598            query,
599            qualification.qualification_digest(),
600            checkpoint,
601            checkpoint_digest,
602            acceptance,
603            None,
604        ),
605        ProviderAttemptPhaseV1::Executing {
606            acceptance,
607            execution_lease,
608            ..
609        } => {
610            if query.observed_at >= execution_lease.expires_at {
611                return Ok(unknown(query, DispatchUnknownReason::StaleExecutionLease));
612            }
613            verified_accepted(
614                provider,
615                query,
616                qualification.qualification_digest(),
617                checkpoint,
618                checkpoint_digest,
619                acceptance,
620                Some(execution_lease),
621            )
622        }
623        ProviderAttemptPhaseV1::Completed { completion, .. } => {
624            let completion = *completion;
625            let fetched = match provider.fetch_completed_outcome(&completion) {
626                Ok(fetched) => fetched,
627                Err(_) => {
628                    return Ok(unknown(
629                        query,
630                        DispatchUnknownReason::CompletedOutcomeUnavailable,
631                    ));
632                }
633            };
634            if fetched.binding != completion
635                || fetched.cost_units != completion.cost_units
636                || fetched.currency != completion.currency
637                || !bytes_match(
638                    &fetched.outcome_bytes,
639                    completion.outcome_size_bytes,
640                    &completion.outcome_sha256,
641                )
642                || !bytes_match(
643                    &fetched.terminal_evidence,
644                    completion.terminal_evidence_size_bytes,
645                    &completion.terminal_evidence_sha256,
646                )
647            {
648                return Ok(unknown(
649                    query,
650                    DispatchUnknownReason::InvalidProviderEvidence,
651                ));
652            }
653            Ok(VerifiedDispatchStatus::Completed(
654                VerifiedProviderCompleted {
655                    qualification_digest: qualification.qualification_digest.clone(),
656                    checkpoint,
657                    checkpoint_digest,
658                    completion,
659                    outcome_bytes: fetched.outcome_bytes,
660                    cost_units: fetched.cost_units,
661                    currency: fetched.currency,
662                    terminal_evidence: fetched.terminal_evidence,
663                },
664            ))
665        }
666    }
667}
668
669fn phase_observed_after(phase: &ProviderAttemptPhaseV1, observed_at: u64) -> bool {
670    match phase {
671        ProviderAttemptPhaseV1::Pending { .. } => false,
672        ProviderAttemptPhaseV1::Accepted { acceptance, .. } => acceptance.accepted_at > observed_at,
673        ProviderAttemptPhaseV1::Cancelled { cancellation, .. } => {
674            cancellation.cancelled_at > observed_at
675        }
676        ProviderAttemptPhaseV1::Executing {
677            acceptance,
678            execution_lease,
679            ..
680        } => acceptance.accepted_at > observed_at || execution_lease.acquired_at > observed_at,
681        ProviderAttemptPhaseV1::Completed {
682            acceptance,
683            execution_lease,
684            completion,
685            ..
686        } => {
687            acceptance.accepted_at > observed_at
688                || execution_lease.acquired_at > observed_at
689                || completion.completed_at > observed_at
690        }
691    }
692}
693
694fn verified_accepted(
695    provider: &dyn DispatchStatusProvider,
696    query: &DispatchStatusQuery,
697    qualification_digest: &str,
698    checkpoint: ProviderAttemptCheckpointV1,
699    checkpoint_digest: String,
700    acceptance: ProviderAcceptanceBindingV1,
701    execution_lease: Option<ProviderExecutionLeaseBindingV1>,
702) -> Result<VerifiedDispatchStatus, DispatchStatusError> {
703    let fetched = match provider.fetch_acceptance(&acceptance) {
704        Ok(fetched) => fetched,
705        Err(_) => return Ok(unknown(query, DispatchUnknownReason::AcceptanceUnavailable)),
706    };
707    if fetched.binding != acceptance
708        || !bytes_match(
709            &fetched.envelope,
710            acceptance.acceptance_envelope_size_bytes,
711            &acceptance.acceptance_envelope_sha256,
712        )
713    {
714        return Ok(unknown(
715            query,
716            DispatchUnknownReason::InvalidProviderEvidence,
717        ));
718    }
719    Ok(VerifiedDispatchStatus::Accepted(VerifiedProviderAccepted {
720        qualification_digest: qualification_digest.to_string(),
721        checkpoint,
722        checkpoint_digest,
723        acceptance,
724        acceptance_envelope: fetched.envelope,
725        execution_lease,
726    }))
727}
728
729fn unknown(query: &DispatchStatusQuery, reason: DispatchUnknownReason) -> VerifiedDispatchStatus {
730    VerifiedDispatchStatus::Unknown(VerifiedProviderUnknown {
731        attempt: query.attempt.clone(),
732        reason,
733        last_checkpoint_digest: query
734            .last_checkpoint
735            .as_ref()
736            .and_then(|checkpoint| checkpoint.digest().ok()),
737    })
738}
739
740fn bytes_match(bytes: &[u8], expected_size: u64, expected_sha256: &str) -> bool {
741    u64::try_from(bytes.len()) == Ok(expected_size) && sha256_hex(bytes) == expected_sha256
742}
743
744#[cfg(test)]
745fn validate_transport_binding(
746    transport_id: &str,
747    transport_key_epoch: u64,
748) -> Result<(), DispatchStatusError> {
749    if transport_id.is_empty()
750        || transport_id.trim() != transport_id
751        || transport_id.len() > 512
752        || transport_id.chars().any(char::is_control)
753    {
754        return Err(DispatchStatusError::InvalidQualification(
755            "transport_id is empty, padded, invalid, or oversized".to_string(),
756        ));
757    }
758    if transport_key_epoch == 0 {
759        return Err(DispatchStatusError::InvalidQualification(
760            "transport_key_epoch must be positive".to_string(),
761        ));
762    }
763    Ok(())
764}
765
766#[cfg(test)]
767fn valid_qualification_id(value: &str) -> bool {
768    !value.is_empty()
769        && value.trim() == value
770        && value.len() <= 512
771        && !value.chars().any(char::is_control)
772}
773
774#[cfg(test)]
775#[path = "dispatch_status_tests.rs"]
776mod tests;