Skip to main content

coven_protocol/provider/
probe.rs

1use super::*;
2pub(super) use crate::store_commit::domain_json;
3
4#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
5pub struct ProviderProbeId([u8; 32]);
6
7impl ProviderProbeId {
8    pub fn from_bytes(bytes: [u8; 32]) -> Self {
9        Self(bytes)
10    }
11
12    pub fn as_bytes(&self) -> &[u8; 32] {
13        &self.0
14    }
15}
16
17impl fmt::Debug for ProviderProbeId {
18    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
19        formatter.write_str(&hex::encode(self.0))
20    }
21}
22
23impl Serialize for ProviderProbeId {
24    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
25    where
26        S: Serializer,
27    {
28        serializer.serialize_str(&hex::encode(self.0))
29    }
30}
31
32impl<'de> Deserialize<'de> for ProviderProbeId {
33    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
34    where
35        D: Deserializer<'de>,
36    {
37        let value = String::deserialize(deserializer)?;
38        if value.len() != 64
39            || value
40                .bytes()
41                .any(|byte| !byte.is_ascii_digit() && !(b'a'..=b'f').contains(&byte))
42        {
43            return Err(serde::de::Error::custom(
44                "provider probe id must be 64 lowercase hexadecimal characters",
45            ));
46        }
47        let bytes: [u8; 32] = hex::decode(value)
48            .map_err(serde::de::Error::custom)?
49            .try_into()
50            .map_err(|_| serde::de::Error::custom("provider probe id has the wrong length"))?;
51        Ok(Self(bytes))
52    }
53}
54
55#[derive(Clone, Copy)]
56pub enum ProbePayloadLabel {
57    ExactCreateFirst,
58    ExactCreateSecond,
59    ConditionalInitial,
60    ConditionalFirst,
61    ConditionalSecond,
62    LostResponse,
63    CrossAdministrator,
64}
65
66impl ProbePayloadLabel {
67    fn bytes(self) -> &'static [u8] {
68        match self {
69            Self::ExactCreateFirst => b"exact-create-first",
70            Self::ExactCreateSecond => b"exact-create-second",
71            Self::ConditionalInitial => b"conditional-initial",
72            Self::ConditionalFirst => b"conditional-first",
73            Self::ConditionalSecond => b"conditional-second",
74            Self::LostResponse => b"lost-response",
75            Self::CrossAdministrator => b"cross-administrator",
76        }
77    }
78}
79
80pub fn probe_payload(probe_id: &ProviderProbeId, label: ProbePayloadLabel) -> Vec<u8> {
81    let mut output = Vec::with_capacity(PROBE_PAYLOAD_LEN);
82    let mut counter = 0u32;
83    while output.len() < PROBE_PAYLOAD_LEN {
84        let mut digest = Sha256::new();
85        digest.update(PAYLOAD_DOMAIN);
86        digest.update(probe_id.as_bytes());
87        digest.update(label.bytes());
88        digest.update(counter.to_be_bytes());
89        output.extend_from_slice(&digest.finalize());
90        counter += 1;
91    }
92    output.truncate(PROBE_PAYLOAD_LEN);
93    output
94}
95
96#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(deny_unknown_fields)]
98pub struct ExactSlotProbeReceipt {
99    pub transcript: ExactSlotProbeTranscript,
100    pub transcript_hash: ObjectHash,
101}
102
103impl ExactSlotProbeReceipt {
104    pub fn from_transcript(
105        transcript: ExactSlotProbeTranscript,
106        store: &StoreProviderBinding,
107        device: &ProviderDeviceBinding,
108    ) -> Self {
109        let transcript_hash = exact_transcript_hash(store, device, &transcript);
110        Self {
111            transcript,
112            transcript_hash,
113        }
114    }
115
116    pub fn verify(
117        &self,
118        store: &StoreProviderBinding,
119        device: &ProviderDeviceBinding,
120    ) -> Result<(), ProviderProbeError> {
121        store.validate().map_err(ProviderProbeError::Storage)?;
122        device
123            .validate_for(store)
124            .map_err(ProviderProbeError::Storage)?;
125        let t = &self.transcript;
126        if self.transcript_hash != exact_transcript_hash(store, device, t) {
127            return invalid("exact-slot transcript hash does not match its context");
128        }
129        if t.logical_key != t.slot.logical_key() || t.accepted.slot() != &t.slot {
130            return invalid("exact-slot transcript disagrees with its allocated slot");
131        }
132        let payloads = [
133            probe_payload(&t.probe_id, ProbePayloadLabel::ExactCreateFirst),
134            probe_payload(&t.probe_id, ProbePayloadLabel::ExactCreateSecond),
135        ];
136        let expected_hashes = [
137            ObjectHash::digest(&payloads[0]),
138            ObjectHash::digest(&payloads[1]),
139        ];
140        if t.contenders[0].payload_hash != expected_hashes[0]
141            || t.contenders[1].payload_hash != expected_hashes[1]
142        {
143            return invalid("exact-slot contender payload hashes are not deterministic");
144        }
145        let winners: Vec<_> = t
146            .contenders
147            .iter()
148            .enumerate()
149            .filter_map(|(index, attempt)| {
150                (attempt.outcome == ProbeCreateOutcome::Created).then_some(index)
151            })
152            .collect();
153        let rejected = t
154            .contenders
155            .iter()
156            .filter(|attempt| attempt.outcome == ProbeCreateOutcome::RejectedOccupied)
157            .count();
158        if winners.len() != 1 || rejected != 1 {
159            return invalid("exact-slot race must contain one create and one occupied rejection");
160        }
161        let winner = &payloads[winners[0]];
162        if t.accepted.stored_size() != winner.len() as u64
163            || t.accepted.stored_hash() != ObjectHash::digest(winner)
164            || t.full_read_hash != ObjectHash::digest(winner)
165            || t.range.start != PROBE_RANGE_START
166            || t.range.end != PROBE_RANGE_END
167            || t.range.bytes_hash
168                != ObjectHash::digest(&winner[PROBE_RANGE_START as usize..PROBE_RANGE_END as usize])
169        {
170            return invalid("exact-slot read, range, reference, or deletion evidence is invalid");
171        }
172        t.conditional.verify(&t.probe_id)?;
173        let lost = probe_payload(&t.probe_id, ProbePayloadLabel::LostResponse);
174        let lost_hash = ObjectHash::digest(&lost);
175        if t.lost_response.logical_key != t.lost_response.slot.logical_key()
176            || t.lost_response.settled.slot() != &t.lost_response.slot
177            || t.lost_response.payload_hash != lost_hash
178            || t.lost_response.settled.stored_size() != lost.len() as u64
179            || t.lost_response.settled.stored_hash() != lost_hash
180            || t.lost_response.readback_hash != lost_hash
181        {
182            return invalid("lost-response exact-slot evidence is invalid");
183        }
184        Ok(())
185    }
186}
187
188#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(deny_unknown_fields)]
190pub struct ExactSlotProbeTranscript {
191    pub probe_id: ProviderProbeId,
192    pub logical_key: String,
193    pub slot: ObjectSlot,
194    pub contenders: [ProbeCreateAttempt; 2],
195    pub accepted: ExactObjectRef,
196    pub full_read_hash: ObjectHash,
197    pub range: ProbeRangeReceipt,
198    pub conditional: ConditionalUpdateProbeReceipt,
199    pub lost_response: LostResponseProbeReceipt,
200}
201
202#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(deny_unknown_fields)]
204pub struct ConditionalUpdateProbeReceipt {
205    pub logical_key: String,
206    pub slot: ObjectSlot,
207    pub starting_payload_hash: ObjectHash,
208    pub contenders: [ProbeConditionalAttempt; 2],
209    pub accepted_payload_hash: ObjectHash,
210}
211
212impl ConditionalUpdateProbeReceipt {
213    pub(super) fn verify(&self, probe_id: &ProviderProbeId) -> Result<(), ProviderProbeError> {
214        if self.logical_key != self.slot.logical_key() {
215            return invalid("conditional-update transcript disagrees with its allocated slot");
216        }
217        let initial = probe_payload(probe_id, ProbePayloadLabel::ConditionalInitial);
218        let payloads = [
219            probe_payload(probe_id, ProbePayloadLabel::ConditionalFirst),
220            probe_payload(probe_id, ProbePayloadLabel::ConditionalSecond),
221        ];
222        let allowed_starting_hashes = [
223            ObjectHash::digest(&initial),
224            ObjectHash::digest(&payloads[0]),
225            ObjectHash::digest(&payloads[1]),
226        ];
227        if !allowed_starting_hashes.contains(&self.starting_payload_hash)
228            || self.contenders[0].payload_hash != ObjectHash::digest(&payloads[0])
229            || self.contenders[1].payload_hash != ObjectHash::digest(&payloads[1])
230        {
231            return invalid("conditional-update payload hashes are not deterministic");
232        }
233        let winners = self
234            .contenders
235            .iter()
236            .enumerate()
237            .filter_map(|(index, attempt)| {
238                (attempt.outcome == ProbeConditionalOutcome::Replaced).then_some(index)
239            })
240            .collect::<Vec<_>>();
241        let rejected = self
242            .contenders
243            .iter()
244            .filter(|attempt| attempt.outcome == ProbeConditionalOutcome::RejectedRevision)
245            .count();
246        if winners.len() != 1
247            || rejected != 1
248            || self.accepted_payload_hash != ObjectHash::digest(&payloads[winners[0]])
249        {
250            return invalid(
251                "conditional-update race must contain one replacement and one revision rejection",
252            );
253        }
254        Ok(())
255    }
256}
257
258#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
259#[serde(deny_unknown_fields)]
260pub struct ProbeConditionalAttempt {
261    pub payload_hash: ObjectHash,
262    pub outcome: ProbeConditionalOutcome,
263}
264
265#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
266#[serde(rename_all = "snake_case")]
267pub enum ProbeConditionalOutcome {
268    Replaced,
269    RejectedRevision,
270}
271
272#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
273#[serde(deny_unknown_fields)]
274pub struct ProbeCreateAttempt {
275    pub payload_hash: ObjectHash,
276    pub outcome: ProbeCreateOutcome,
277}
278
279#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(rename_all = "snake_case")]
281pub enum ProbeCreateOutcome {
282    Created,
283    RejectedOccupied,
284}
285
286#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
287#[serde(deny_unknown_fields)]
288pub struct LostResponseProbeReceipt {
289    pub logical_key: String,
290    pub slot: ObjectSlot,
291    pub payload_hash: ObjectHash,
292    pub settled: ExactObjectRef,
293    pub readback_hash: ObjectHash,
294}
295
296#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
297#[serde(deny_unknown_fields)]
298pub struct ProbeRangeReceipt {
299    pub start: u64,
300    pub end: u64,
301    pub bytes_hash: ObjectHash,
302}
303
304#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
305#[serde(deny_unknown_fields)]
306pub struct ProbeExactObjectReceipt {
307    pub slot: ObjectSlot,
308    pub payload_hash: ObjectHash,
309    pub object: ExactObjectRef,
310}
311
312#[derive(Debug, thiserror::Error)]
313pub enum ProviderProbeError {
314    #[error(transparent)]
315    Storage(#[from] StorageError),
316    #[error("provider capability receipt Store protocol: {0}")]
317    Protocol(#[from] crate::store_commit::StoreProtocolError),
318    #[error("provider capability receipt is invalid: {0}")]
319    InvalidReceipt(String),
320}
321
322#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
323#[serde(rename_all = "snake_case", deny_unknown_fields)]
324pub enum ProviderProbeJournalRecord {
325    Exact(ExactProbeJournal),
326    CrossPrincipal(CrossPrincipalCompletionJournal),
327}
328
329impl ProviderProbeJournalRecord {
330    pub fn probe_id(&self) -> ProviderProbeId {
331        match self {
332            Self::Exact(record) => record.probe_id,
333            Self::CrossPrincipal(record) => record.probe_id,
334        }
335    }
336
337    pub fn validate_begin(&self) -> Result<(), ProviderProbeJournalError> {
338        let prepared = match self {
339            Self::Exact(record) => matches!(record.progress, ExactProbeProgress::Prepared),
340            Self::CrossPrincipal(record) => {
341                matches!(record.progress, CrossPrincipalCompletionProgress::Prepared)
342            }
343        };
344        if !prepared {
345            return Err(ProviderProbeJournalError::BeginNotPrepared);
346        }
347        Ok(())
348    }
349
350    pub fn validate_transition(&self, next: &Self) -> Result<(), ProviderProbeJournalError> {
351        match (self, next) {
352            (Self::Exact(previous), Self::Exact(next)) => {
353                if previous.probe_id != next.probe_id
354                    || previous.binding != next.binding
355                    || previous.slot != next.slot
356                    || previous.conditional_slot != next.conditional_slot
357                    || previous.lost_response_slot != next.lost_response_slot
358                {
359                    return Err(ProviderProbeJournalError::ImmutableFactsChanged);
360                }
361                validate_exact_progress_transition(&previous.progress, &next.progress)
362            }
363            (Self::CrossPrincipal(previous), Self::CrossPrincipal(next)) => {
364                if previous.probe_id != next.probe_id
365                    || previous.store != next.store
366                    || previous.context != next.context
367                    || previous.challenge != next.challenge
368                    || previous.response != next.response
369                {
370                    return Err(ProviderProbeJournalError::ImmutableFactsChanged);
371                }
372                if let Some((read_hash, conditional)) = cross_progress_evidence(&next.progress) {
373                    let transcript = CrossPrincipalProbeTranscript {
374                        challenge: next.challenge.clone(),
375                        response: next.response.clone(),
376                        administrator_read_peer_hash: read_hash,
377                        conditional: conditional.clone(),
378                    };
379                    validate_cross_transcript_payloads(&transcript, &next.context)
380                        .map_err(|_| ProviderProbeJournalError::EvidenceChanged)?;
381                }
382                validate_cross_progress_transition(&previous.progress, &next.progress)
383            }
384            _ => Err(ProviderProbeJournalError::ProbeKindChanged),
385        }
386    }
387}
388
389#[derive(Debug, thiserror::Error, PartialEq, Eq)]
390pub enum ProviderProbeJournalError {
391    #[error("provider probe journal must begin at prepared")]
392    BeginNotPrepared,
393    #[error("provider probe journal advance changes immutable facts")]
394    ImmutableFactsChanged,
395    #[error("provider probe journal advance changes the probe kind")]
396    ProbeKindChanged,
397    #[error("provider probe journal advance skips or reverses progress")]
398    NonAdjacentProgress,
399    #[error("provider probe journal advance changes established evidence")]
400    EvidenceChanged,
401}
402
403#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
404#[serde(deny_unknown_fields)]
405pub struct ExactProbeJournal {
406    pub probe_id: ProviderProbeId,
407    pub binding: crate::objects::ResolvedProviderBinding,
408    pub slot: ObjectSlot,
409    pub conditional_slot: ObjectSlot,
410    pub lost_response_slot: ObjectSlot,
411    pub progress: ExactProbeProgress,
412}
413
414#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
415#[serde(rename_all = "snake_case", deny_unknown_fields)]
416pub enum ExactProbeProgress {
417    Prepared,
418    Created {
419        outcomes: [ProbeCreateOutcome; 2],
420    },
421    ReadsVerified {
422        outcomes: [ProbeCreateOutcome; 2],
423    },
424    ConditionalVerified {
425        outcomes: [ProbeCreateOutcome; 2],
426        conditional: ConditionalUpdateProbeReceipt,
427    },
428    PrimaryAbsent {
429        outcomes: [ProbeCreateOutcome; 2],
430        conditional: ConditionalUpdateProbeReceipt,
431    },
432    LostResponseCreated {
433        outcomes: [ProbeCreateOutcome; 2],
434        conditional: ConditionalUpdateProbeReceipt,
435    },
436    LostResponseReadVerified {
437        outcomes: [ProbeCreateOutcome; 2],
438        conditional: ConditionalUpdateProbeReceipt,
439    },
440    Absent {
441        outcomes: [ProbeCreateOutcome; 2],
442        conditional: ConditionalUpdateProbeReceipt,
443    },
444    ReceiptReady {
445        receipt: ExactSlotProbeReceipt,
446    },
447}
448
449#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
450#[serde(deny_unknown_fields)]
451pub struct CrossPrincipalCompletionJournal {
452    pub probe_id: ProviderProbeId,
453    pub store: StoreProviderBinding,
454    pub context: CrossPrincipalResponseContext,
455    pub challenge: CrossPrincipalProbeChallenge,
456    pub response: CrossPrincipalProbeResponse,
457    pub progress: CrossPrincipalCompletionProgress,
458}
459
460#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
461#[serde(rename_all = "snake_case", deny_unknown_fields)]
462pub enum CrossPrincipalCompletionProgress {
463    Prepared,
464    ReadsVerified {
465        administrator_read_peer_hash: ObjectHash,
466        conditional: ConditionalUpdateProbeReceipt,
467    },
468    ResponseObjectsAbsent {
469        administrator_read_peer_hash: ObjectHash,
470        conditional: ConditionalUpdateProbeReceipt,
471    },
472    Absent {
473        administrator_read_peer_hash: ObjectHash,
474        conditional: ConditionalUpdateProbeReceipt,
475    },
476    ReceiptReady {
477        receipt: CrossPrincipalProbeReceipt,
478    },
479}
480
481pub(super) fn validate_exact_progress_transition(
482    previous: &ExactProbeProgress,
483    next: &ExactProbeProgress,
484) -> Result<(), ProviderProbeJournalError> {
485    let evidence_matches = match (previous, next) {
486        (ExactProbeProgress::Prepared, ExactProbeProgress::Created { .. }) => true,
487        (
488            ExactProbeProgress::Created { outcomes: previous },
489            ExactProbeProgress::ReadsVerified { outcomes: next },
490        ) => previous == next,
491        (
492            ExactProbeProgress::ReadsVerified { outcomes: previous },
493            ExactProbeProgress::ConditionalVerified { outcomes: next, .. },
494        ) => previous == next,
495        (
496            ExactProbeProgress::ConditionalVerified {
497                outcomes: previous_outcomes,
498                conditional: previous_conditional,
499            },
500            ExactProbeProgress::PrimaryAbsent {
501                outcomes: next_outcomes,
502                conditional: next_conditional,
503            },
504        )
505        | (
506            ExactProbeProgress::PrimaryAbsent {
507                outcomes: previous_outcomes,
508                conditional: previous_conditional,
509            },
510            ExactProbeProgress::LostResponseCreated {
511                outcomes: next_outcomes,
512                conditional: next_conditional,
513            },
514        )
515        | (
516            ExactProbeProgress::LostResponseCreated {
517                outcomes: previous_outcomes,
518                conditional: previous_conditional,
519            },
520            ExactProbeProgress::LostResponseReadVerified {
521                outcomes: next_outcomes,
522                conditional: next_conditional,
523            },
524        )
525        | (
526            ExactProbeProgress::LostResponseReadVerified {
527                outcomes: previous_outcomes,
528                conditional: previous_conditional,
529            },
530            ExactProbeProgress::Absent {
531                outcomes: next_outcomes,
532                conditional: next_conditional,
533            },
534        ) => previous_outcomes == next_outcomes && previous_conditional == next_conditional,
535        (
536            ExactProbeProgress::Absent {
537                outcomes,
538                conditional,
539            },
540            ExactProbeProgress::ReceiptReady { receipt },
541        ) => {
542            receipt
543                .transcript
544                .contenders
545                .iter()
546                .map(|attempt| attempt.outcome)
547                .eq(outcomes.iter().copied())
548                && receipt.transcript.conditional == *conditional
549        }
550        _ => return Err(ProviderProbeJournalError::NonAdjacentProgress),
551    };
552    if !evidence_matches {
553        return Err(ProviderProbeJournalError::EvidenceChanged);
554    }
555    Ok(())
556}
557
558pub(super) fn validate_cross_progress_transition(
559    previous: &CrossPrincipalCompletionProgress,
560    next: &CrossPrincipalCompletionProgress,
561) -> Result<(), ProviderProbeJournalError> {
562    let adjacent = matches!(
563        (previous, next),
564        (
565            CrossPrincipalCompletionProgress::Prepared,
566            CrossPrincipalCompletionProgress::ReadsVerified { .. }
567        ) | (
568            CrossPrincipalCompletionProgress::ReadsVerified { .. },
569            CrossPrincipalCompletionProgress::ResponseObjectsAbsent { .. }
570        ) | (
571            CrossPrincipalCompletionProgress::ResponseObjectsAbsent { .. },
572            CrossPrincipalCompletionProgress::Absent { .. }
573        ) | (
574            CrossPrincipalCompletionProgress::Absent { .. },
575            CrossPrincipalCompletionProgress::ReceiptReady { .. }
576        )
577    );
578    if !adjacent {
579        return Err(ProviderProbeJournalError::NonAdjacentProgress);
580    }
581    if let Some(previous) = cross_progress_evidence(previous) {
582        if Some(previous) != cross_progress_evidence(next) {
583            return Err(ProviderProbeJournalError::EvidenceChanged);
584        }
585    }
586    Ok(())
587}
588
589pub(super) fn cross_progress_evidence(
590    progress: &CrossPrincipalCompletionProgress,
591) -> Option<(ObjectHash, &ConditionalUpdateProbeReceipt)> {
592    match progress {
593        CrossPrincipalCompletionProgress::Prepared => None,
594        CrossPrincipalCompletionProgress::ReadsVerified {
595            administrator_read_peer_hash,
596            conditional,
597        }
598        | CrossPrincipalCompletionProgress::ResponseObjectsAbsent {
599            administrator_read_peer_hash,
600            conditional,
601        }
602        | CrossPrincipalCompletionProgress::Absent {
603            administrator_read_peer_hash,
604            conditional,
605        } => Some((*administrator_read_peer_hash, conditional)),
606        CrossPrincipalCompletionProgress::ReceiptReady { receipt } => Some((
607            receipt.transcript.administrator_read_peer_hash,
608            &receipt.transcript.conditional,
609        )),
610    }
611}
612
613#[async_trait]
614pub trait ProviderProbeJournal: Send + Sync {
615    async fn load(
616        &self,
617        probe_id: ProviderProbeId,
618    ) -> Result<Option<ProviderProbeJournalRecord>, StorageError>;
619
620    /// Atomically inserts `prepared` when absent or returns the exact existing
621    /// record for this probe id. A different record under the id is corruption.
622    async fn begin(
623        &self,
624        prepared: ProviderProbeJournalRecord,
625    ) -> Result<ProviderProbeJournalRecord, StorageError>;
626
627    /// Atomically replaces the exact current record. Implementations reject a
628    /// stale predecessor instead of merging progress.
629    async fn advance(
630        &self,
631        previous: &ProviderProbeJournalRecord,
632        next: ProviderProbeJournalRecord,
633    ) -> Result<(), StorageError>;
634}
635
636pub(super) fn validate_probe_exact_object(
637    receipt: &ProbeExactObjectReceipt,
638    expected_logical_key: &str,
639    payload: &[u8],
640    label: &str,
641) -> Result<(), ProviderProbeError> {
642    let payload_hash = ObjectHash::digest(payload);
643    if receipt.slot.logical_key() != expected_logical_key
644        || receipt.slot != *receipt.object.slot()
645        || receipt.payload_hash != payload_hash
646        || receipt.object.stored_size() != payload.len() as u64
647        || receipt.object.stored_hash() != payload_hash
648    {
649        return invalid(&format!(
650            "{label} object reference or payload hash is invalid"
651        ));
652    }
653    Ok(())
654}
655
656pub fn invalid<T>(reason: &str) -> Result<T, ProviderProbeError> {
657    Err(ProviderProbeError::InvalidReceipt(reason.to_string()))
658}
659
660pub(super) fn exact_transcript_hash(
661    store: &StoreProviderBinding,
662    device: &ProviderDeviceBinding,
663    transcript: &ExactSlotProbeTranscript,
664) -> ObjectHash {
665    ObjectHash::digest(&domain_json(
666        EXACT_TRANSCRIPT_DOMAIN,
667        &(store, device, transcript),
668    ))
669}