1use 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 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#[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
118pub 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 fn fetch_completed_outcome(
146 &self,
147 binding: &ProviderCompletionBindingV1,
148 ) -> Result<AuthenticatedProviderCompletedOutcome, DispatchStatusProviderError>;
149}
150
151trait QualifiedDispatchStatusAdapter: DispatchStatusProvider {}
159
160pub 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;