Skip to main content

chio_listing/outcome/
delivery.rs

1use chio_core_types::crypto::{Keypair, PublicKey};
2use chio_core_types::receipt::lineage::SignedExportEnvelope;
3use serde::{Deserialize, Serialize};
4
5use super::{
6    contract::authenticate_outcome_eligibility_at, domain_digest, domain_digest_without_field,
7    envelope_digest, load_canonical_outcome_json, validate_digest, validate_text, validate_time,
8    AuthenticatedOutcomeEligibilityV1, OutcomeEligibilityAuthenticationV1, OutcomeError,
9    OutcomeSignerTrustV1,
10};
11
12pub const OUTCOME_DELIVERY_CHECKPOINT_SCHEMA: &str =
13    chio_core_types::CHIO_OUTCOME_DELIVERY_CHECKPOINT_V1_SCHEMA;
14pub const OUTCOME_DELIVERY_ACKNOWLEDGEMENT_SCHEMA: &str =
15    chio_core_types::CHIO_OUTCOME_DELIVERY_ACKNOWLEDGEMENT_V1_SCHEMA;
16pub const OUTCOME_DELIVERY_NONACCEPTANCE_SCHEMA: &str =
17    chio_core_types::CHIO_OUTCOME_DELIVERY_NONACCEPTANCE_V1_SCHEMA;
18
19const ACK_ID_DOMAIN: &[u8] = b"chio.outcome.delivery-acknowledgement.id.v1\0";
20const NONACCEPTANCE_ID_DOMAIN: &[u8] = b"chio.outcome.delivery-nonacceptance.id.v1\0";
21const RECEIVER_BINDING_DOMAIN: &[u8] = b"chio.outcome.receiver-binding.v1\0";
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum OutcomeDeliveryCheckpointStateV1 {
26    Pending,
27    Acknowledged,
28    Cancelled,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct OutcomeDeliveryCheckpointInputV1 {
33    pub anchor_id: String,
34    pub anchor_key_epoch: u64,
35    pub receiver_binding_digest: String,
36    pub receiver_id: String,
37    pub receiver_namespace: String,
38    pub receiver_key_epoch: u64,
39    pub delivery_id: String,
40    pub idempotency_key: String,
41    pub receiver_queue_id: String,
42    pub request_id: String,
43    pub eligibility_digest: String,
44    pub provider_acceptance_digest: String,
45    pub output_digest: String,
46    pub trusted_clock_high_water_unix_ms: u64,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct OutcomeReceiverBindingV1 {
51    receiver_binding_digest: String,
52    anchor_id: String,
53    anchor_key: PublicKey,
54    anchor_key_epoch: u64,
55    receiver_id: String,
56    receiver_namespace: String,
57    receiver_key_id: String,
58    receiver_key: PublicKey,
59    receiver_key_epoch: u64,
60}
61
62impl OutcomeReceiverBindingV1 {
63    pub fn new(
64        receiver_id: String,
65        receiver_namespace: String,
66        anchor_trust: &OutcomeSignerTrustV1,
67        receiver_trust: &OutcomeSignerTrustV1,
68    ) -> Result<Self, OutcomeError> {
69        validate_text("receiver_id", &receiver_id)?;
70        validate_text("receiver_namespace", &receiver_namespace)?;
71        let receiver_binding_digest = domain_digest(
72            RECEIVER_BINDING_DOMAIN,
73            &OutcomeReceiverBindingPreimageV1 {
74                anchor_id: anchor_trust.principal_id(),
75                anchor_key: anchor_trust.key(),
76                anchor_key_epoch: anchor_trust.key_epoch(),
77                receiver_id: &receiver_id,
78                receiver_namespace: &receiver_namespace,
79                receiver_key_id: receiver_trust.principal_id(),
80                receiver_key: receiver_trust.key(),
81                receiver_key_epoch: receiver_trust.key_epoch(),
82            },
83        )?;
84        Ok(Self {
85            receiver_binding_digest,
86            anchor_id: anchor_trust.principal_id().to_owned(),
87            anchor_key: anchor_trust.key().clone(),
88            anchor_key_epoch: anchor_trust.key_epoch(),
89            receiver_id,
90            receiver_namespace,
91            receiver_key_id: receiver_trust.principal_id().to_owned(),
92            receiver_key: receiver_trust.key().clone(),
93            receiver_key_epoch: receiver_trust.key_epoch(),
94        })
95    }
96
97    #[must_use]
98    pub fn digest(&self) -> &str {
99        &self.receiver_binding_digest
100    }
101}
102
103#[derive(Serialize)]
104#[serde(rename_all = "camelCase")]
105struct OutcomeReceiverBindingPreimageV1<'a> {
106    anchor_id: &'a str,
107    anchor_key: &'a PublicKey,
108    anchor_key_epoch: u64,
109    receiver_id: &'a str,
110    receiver_namespace: &'a str,
111    receiver_key_id: &'a str,
112    receiver_key: &'a PublicKey,
113    receiver_key_epoch: u64,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase", deny_unknown_fields)]
118pub struct OutcomeDeliveryCheckpointBodyV1 {
119    schema: String,
120    anchor_id: String,
121    anchor_key_epoch: u64,
122    receiver_binding_digest: String,
123    receiver_id: String,
124    receiver_namespace: String,
125    receiver_key_epoch: u64,
126    delivery_id: String,
127    idempotency_key: String,
128    receiver_queue_id: String,
129    sequence: u64,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    predecessor_digest: Option<String>,
132    state: OutcomeDeliveryCheckpointStateV1,
133    request_id: String,
134    eligibility_digest: String,
135    provider_acceptance_digest: String,
136    output_digest: String,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    blob_reference: Option<String>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    blob_digest: Option<String>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    blob_absence_proof_digest: Option<String>,
143    #[serde(skip_serializing_if = "Option::is_none")]
144    cancellation_fence_digest: Option<String>,
145    trusted_clock_high_water_unix_ms: u64,
146}
147
148impl OutcomeDeliveryCheckpointBodyV1 {
149    pub fn pending_from_anchor_assertion(
150        input: OutcomeDeliveryCheckpointInputV1,
151    ) -> Result<Self, OutcomeError> {
152        let body = Self {
153            schema: OUTCOME_DELIVERY_CHECKPOINT_SCHEMA.to_owned(),
154            anchor_id: input.anchor_id,
155            anchor_key_epoch: input.anchor_key_epoch,
156            receiver_binding_digest: input.receiver_binding_digest,
157            receiver_id: input.receiver_id,
158            receiver_namespace: input.receiver_namespace,
159            receiver_key_epoch: input.receiver_key_epoch,
160            delivery_id: input.delivery_id,
161            idempotency_key: input.idempotency_key,
162            receiver_queue_id: input.receiver_queue_id,
163            sequence: 1,
164            predecessor_digest: None,
165            state: OutcomeDeliveryCheckpointStateV1::Pending,
166            request_id: input.request_id,
167            eligibility_digest: input.eligibility_digest,
168            provider_acceptance_digest: input.provider_acceptance_digest,
169            output_digest: input.output_digest,
170            blob_reference: None,
171            blob_digest: None,
172            blob_absence_proof_digest: None,
173            cancellation_fence_digest: None,
174            trusted_clock_high_water_unix_ms: input.trusted_clock_high_water_unix_ms,
175        };
176        body.validate()?;
177        Ok(body)
178    }
179
180    pub fn validate(&self) -> Result<(), OutcomeError> {
181        if self.schema != OUTCOME_DELIVERY_CHECKPOINT_SCHEMA {
182            return Err(OutcomeError::InvalidField("delivery_checkpoint_schema"));
183        }
184        for (field, value) in [
185            ("anchor_id", &self.anchor_id),
186            ("receiver_id", &self.receiver_id),
187            ("receiver_namespace", &self.receiver_namespace),
188            ("delivery_id", &self.delivery_id),
189            ("idempotency_key", &self.idempotency_key),
190            ("receiver_queue_id", &self.receiver_queue_id),
191            ("request_id", &self.request_id),
192        ] {
193            validate_text(field, value)?;
194        }
195        for (field, value) in [
196            ("receiver_binding_digest", &self.receiver_binding_digest),
197            ("eligibility_digest", &self.eligibility_digest),
198            (
199                "provider_acceptance_digest",
200                &self.provider_acceptance_digest,
201            ),
202            ("output_digest", &self.output_digest),
203        ] {
204            validate_digest(field, value)?;
205        }
206        for (field, value) in [
207            ("anchor_key_epoch", self.anchor_key_epoch),
208            ("receiver_key_epoch", self.receiver_key_epoch),
209            ("sequence", self.sequence),
210            (
211                "trusted_clock_high_water_unix_ms",
212                self.trusted_clock_high_water_unix_ms,
213            ),
214        ] {
215            validate_time(field, value)?;
216        }
217        if let Some(digest) = &self.predecessor_digest {
218            validate_digest("predecessor_digest", digest)?;
219        }
220        if let Some(reference) = &self.blob_reference {
221            validate_text("blob_reference", reference)?;
222        }
223        for (field, digest) in [
224            ("blob_digest", self.blob_digest.as_deref()),
225            (
226                "blob_absence_proof_digest",
227                self.blob_absence_proof_digest.as_deref(),
228            ),
229            (
230                "cancellation_fence_digest",
231                self.cancellation_fence_digest.as_deref(),
232            ),
233        ] {
234            if let Some(digest) = digest {
235                validate_digest(field, digest)?;
236            }
237        }
238        if self.state == OutcomeDeliveryCheckpointStateV1::Acknowledged
239            && self.blob_digest.as_deref() != Some(self.output_digest.as_str())
240        {
241            return Err(OutcomeError::BindingMismatch);
242        }
243        let shape_valid = match self.state {
244            OutcomeDeliveryCheckpointStateV1::Pending => {
245                self.sequence == 1
246                    && self.predecessor_digest.is_none()
247                    && self.blob_reference.is_none()
248                    && self.blob_digest.is_none()
249                    && self.blob_absence_proof_digest.is_none()
250                    && self.cancellation_fence_digest.is_none()
251            }
252            OutcomeDeliveryCheckpointStateV1::Acknowledged => {
253                self.sequence > 1
254                    && self.predecessor_digest.is_some()
255                    && self.blob_reference.is_some()
256                    && self.blob_digest.is_some()
257                    && self.blob_absence_proof_digest.is_none()
258                    && self.cancellation_fence_digest.is_none()
259            }
260            OutcomeDeliveryCheckpointStateV1::Cancelled => {
261                self.sequence > 1
262                    && self.predecessor_digest.is_some()
263                    && self.blob_reference.is_none()
264                    && self.blob_digest.is_none()
265                    && self.blob_absence_proof_digest.is_some()
266                    && self.cancellation_fence_digest.is_some()
267            }
268        };
269        if !shape_valid {
270            return Err(OutcomeError::InvalidField("delivery_checkpoint_state"));
271        }
272        Ok(())
273    }
274
275    #[must_use]
276    pub const fn state(&self) -> OutcomeDeliveryCheckpointStateV1 {
277        self.state
278    }
279
280    #[must_use]
281    pub const fn sequence(&self) -> u64 {
282        self.sequence
283    }
284
285    #[must_use]
286    pub fn receiver_binding_digest(&self) -> &str {
287        &self.receiver_binding_digest
288    }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292#[serde(transparent)]
293pub struct SignedOutcomeDeliveryCheckpointV1(SignedExportEnvelope<OutcomeDeliveryCheckpointBodyV1>);
294
295impl SignedOutcomeDeliveryCheckpointV1 {
296    pub fn sign(
297        body: OutcomeDeliveryCheckpointBodyV1,
298        signer: &Keypair,
299    ) -> Result<Self, OutcomeError> {
300        body.validate()?;
301        SignedExportEnvelope::sign(body, signer)
302            .map(Self)
303            .map_err(|error| OutcomeError::Canonicalization(error.to_string()))
304    }
305
306    #[must_use]
307    pub const fn body(&self) -> &OutcomeDeliveryCheckpointBodyV1 {
308        &self.0.body
309    }
310}
311
312#[derive(Debug, Clone)]
313pub struct AuthenticatedOutcomeDeliveryCheckpointV1 {
314    signed: SignedOutcomeDeliveryCheckpointV1,
315    envelope_digest: String,
316    receiver_binding: OutcomeReceiverBindingV1,
317}
318
319impl AuthenticatedOutcomeDeliveryCheckpointV1 {
320    #[must_use]
321    pub const fn body(&self) -> &OutcomeDeliveryCheckpointBodyV1 {
322        self.signed.body()
323    }
324
325    #[must_use]
326    pub fn envelope_digest(&self) -> &str {
327        &self.envelope_digest
328    }
329
330    pub fn acknowledgement_assertion(
331        &self,
332        blob_reference: String,
333        blob_digest: String,
334        trusted_clock_high_water_unix_ms: u64,
335    ) -> Result<OutcomeDeliveryCheckpointBodyV1, OutcomeError> {
336        validate_text("blob_reference", &blob_reference)?;
337        validate_digest("blob_digest", &blob_digest)?;
338        if blob_digest != self.body().output_digest {
339            return Err(OutcomeError::BindingMismatch);
340        }
341        self.advance(
342            OutcomeDeliveryCheckpointStateV1::Acknowledged,
343            Some(blob_reference),
344            Some(blob_digest),
345            None,
346            None,
347            trusted_clock_high_water_unix_ms,
348        )
349    }
350
351    pub fn cancellation_assertion(
352        &self,
353        blob_absence_proof_digest: String,
354        cancellation_fence_digest: String,
355        trusted_clock_high_water_unix_ms: u64,
356    ) -> Result<OutcomeDeliveryCheckpointBodyV1, OutcomeError> {
357        validate_digest("blob_absence_proof_digest", &blob_absence_proof_digest)?;
358        validate_digest("cancellation_fence_digest", &cancellation_fence_digest)?;
359        self.advance(
360            OutcomeDeliveryCheckpointStateV1::Cancelled,
361            None,
362            None,
363            Some(blob_absence_proof_digest),
364            Some(cancellation_fence_digest),
365            trusted_clock_high_water_unix_ms,
366        )
367    }
368
369    fn advance(
370        &self,
371        state: OutcomeDeliveryCheckpointStateV1,
372        blob_reference: Option<String>,
373        blob_digest: Option<String>,
374        blob_absence_proof_digest: Option<String>,
375        cancellation_fence_digest: Option<String>,
376        trusted_clock_high_water_unix_ms: u64,
377    ) -> Result<OutcomeDeliveryCheckpointBodyV1, OutcomeError> {
378        if self.body().state != OutcomeDeliveryCheckpointStateV1::Pending
379            || trusted_clock_high_water_unix_ms < self.body().trusted_clock_high_water_unix_ms
380        {
381            return Err(OutcomeError::IllegalTransition);
382        }
383        let mut next = self.body().clone();
384        next.sequence = next
385            .sequence
386            .checked_add(1)
387            .ok_or(OutcomeError::ArithmeticOverflow)?;
388        next.predecessor_digest = Some(self.envelope_digest.clone());
389        next.state = state;
390        next.blob_reference = blob_reference;
391        next.blob_digest = blob_digest;
392        next.blob_absence_proof_digest = blob_absence_proof_digest;
393        next.cancellation_fence_digest = cancellation_fence_digest;
394        next.trusted_clock_high_water_unix_ms = trusted_clock_high_water_unix_ms;
395        next.validate()?;
396        Ok(next)
397    }
398}
399
400pub fn authenticate_outcome_delivery_checkpoint(
401    canonical_envelope: &[u8],
402    trust: &OutcomeSignerTrustV1,
403    receiver_binding: &OutcomeReceiverBindingV1,
404    current: Option<&AuthenticatedOutcomeDeliveryCheckpointV1>,
405) -> Result<AuthenticatedOutcomeDeliveryCheckpointV1, OutcomeError> {
406    let signed: SignedOutcomeDeliveryCheckpointV1 =
407        load_canonical_outcome_json(canonical_envelope)?;
408    signed.body().validate()?;
409    if signed.body().anchor_id != trust.principal_id()
410        || signed.body().anchor_key_epoch != trust.key_epoch()
411        || trust.principal_id() != receiver_binding.anchor_id
412        || trust.key() != &receiver_binding.anchor_key
413        || trust.key_epoch() != receiver_binding.anchor_key_epoch
414        || signed.body().receiver_binding_digest != receiver_binding.receiver_binding_digest
415        || signed.body().receiver_id != receiver_binding.receiver_id
416        || signed.body().receiver_namespace != receiver_binding.receiver_namespace
417        || signed.body().receiver_key_epoch != receiver_binding.receiver_key_epoch
418    {
419        return Err(OutcomeError::BindingMismatch);
420    }
421    if signed.0.signer_key != *trust.key()
422        || !signed
423            .0
424            .verify_signature()
425            .map_err(|error| OutcomeError::Canonicalization(error.to_string()))?
426    {
427        return Err(OutcomeError::AuthorityVerification);
428    }
429    match current {
430        None if signed.body().state == OutcomeDeliveryCheckpointStateV1::Pending => {}
431        Some(current) => validate_checkpoint_advance(current, signed.body())?,
432        None => return Err(OutcomeError::IllegalTransition),
433    }
434    Ok(AuthenticatedOutcomeDeliveryCheckpointV1 {
435        envelope_digest: envelope_digest(&signed)?,
436        signed,
437        receiver_binding: receiver_binding.clone(),
438    })
439}
440
441pub fn authenticate_outcome_eligibility_from_checkpoint(
442    canonical_envelope: &[u8],
443    context: &OutcomeEligibilityAuthenticationV1<'_>,
444    checkpoint: &AuthenticatedOutcomeDeliveryCheckpointV1,
445) -> Result<AuthenticatedOutcomeEligibilityV1, OutcomeError> {
446    let eligibility = authenticate_outcome_eligibility_at(
447        canonical_envelope,
448        context,
449        checkpoint.body().trusted_clock_high_water_unix_ms,
450    )?;
451    if checkpoint.body().request_id != eligibility.body().request_id()
452        || checkpoint.body().eligibility_digest != eligibility.envelope_digest()
453        || checkpoint.body().receiver_binding_digest != eligibility.body().receiver_binding_digest()
454    {
455        return Err(OutcomeError::BindingMismatch);
456    }
457    Ok(eligibility)
458}
459
460fn validate_checkpoint_advance(
461    current: &AuthenticatedOutcomeDeliveryCheckpointV1,
462    candidate: &OutcomeDeliveryCheckpointBodyV1,
463) -> Result<(), OutcomeError> {
464    let previous = current.body();
465    let expected_sequence = previous
466        .sequence
467        .checked_add(1)
468        .ok_or(OutcomeError::ArithmeticOverflow)?;
469    let immutable = candidate.anchor_id == previous.anchor_id
470        && candidate.anchor_key_epoch == previous.anchor_key_epoch
471        && candidate.receiver_binding_digest == previous.receiver_binding_digest
472        && candidate.receiver_id == previous.receiver_id
473        && candidate.receiver_namespace == previous.receiver_namespace
474        && candidate.receiver_key_epoch == previous.receiver_key_epoch
475        && candidate.delivery_id == previous.delivery_id
476        && candidate.idempotency_key == previous.idempotency_key
477        && candidate.receiver_queue_id == previous.receiver_queue_id
478        && candidate.request_id == previous.request_id
479        && candidate.eligibility_digest == previous.eligibility_digest
480        && candidate.provider_acceptance_digest == previous.provider_acceptance_digest
481        && candidate.output_digest == previous.output_digest;
482    if previous.state != OutcomeDeliveryCheckpointStateV1::Pending
483        || !matches!(
484            candidate.state,
485            OutcomeDeliveryCheckpointStateV1::Acknowledged
486                | OutcomeDeliveryCheckpointStateV1::Cancelled
487        )
488        || candidate.sequence != expected_sequence
489        || candidate.predecessor_digest.as_deref() != Some(current.envelope_digest())
490        || candidate.trusted_clock_high_water_unix_ms < previous.trusted_clock_high_water_unix_ms
491        || !immutable
492    {
493        return Err(OutcomeError::IllegalTransition);
494    }
495    Ok(())
496}
497
498#[derive(Debug, Clone, PartialEq, Eq)]
499pub struct OutcomeDeliveryAcknowledgementInputV1 {
500    pub receiver_key_id: String,
501}
502
503#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
504#[serde(rename_all = "camelCase", deny_unknown_fields)]
505pub struct OutcomeDeliveryAcknowledgementBodyV1 {
506    schema: String,
507    acknowledgement_id: String,
508    request_id: String,
509    eligibility_digest: String,
510    provider_acceptance_digest: String,
511    final_output_digest: String,
512    receiver_binding_digest: String,
513    delivery_id: String,
514    idempotency_key: String,
515    receiver_queue_id: String,
516    delivery_accepted_at_unix_ms: u64,
517    receiver_key_id: String,
518    receiver_key_epoch: u64,
519    delivery_checkpoint_sequence: u64,
520    delivery_checkpoint_digest: String,
521    durable_blob_reference: String,
522    durable_blob_digest: String,
523}
524
525impl OutcomeDeliveryAcknowledgementBodyV1 {
526    pub fn from_receiver_assertion(
527        eligibility: &AuthenticatedOutcomeEligibilityV1,
528        checkpoint: &AuthenticatedOutcomeDeliveryCheckpointV1,
529        input: OutcomeDeliveryAcknowledgementInputV1,
530    ) -> Result<Self, OutcomeError> {
531        let checkpoint_body = checkpoint.body();
532        if checkpoint_body.state != OutcomeDeliveryCheckpointStateV1::Acknowledged
533            || checkpoint_body.eligibility_digest != eligibility.envelope_digest()
534            || checkpoint_body.request_id != eligibility.body().request_id()
535            || checkpoint_body.receiver_binding_digest
536                != eligibility.body().receiver_binding_digest()
537        {
538            return Err(OutcomeError::BindingMismatch);
539        }
540        let mut body = Self {
541            schema: OUTCOME_DELIVERY_ACKNOWLEDGEMENT_SCHEMA.to_owned(),
542            acknowledgement_id: String::new(),
543            request_id: checkpoint_body.request_id.clone(),
544            eligibility_digest: checkpoint_body.eligibility_digest.clone(),
545            provider_acceptance_digest: checkpoint_body.provider_acceptance_digest.clone(),
546            final_output_digest: checkpoint_body.output_digest.clone(),
547            receiver_binding_digest: eligibility.body().receiver_binding_digest().to_owned(),
548            delivery_id: checkpoint_body.delivery_id.clone(),
549            idempotency_key: checkpoint_body.idempotency_key.clone(),
550            receiver_queue_id: checkpoint_body.receiver_queue_id.clone(),
551            delivery_accepted_at_unix_ms: checkpoint_body.trusted_clock_high_water_unix_ms,
552            receiver_key_id: input.receiver_key_id,
553            receiver_key_epoch: checkpoint_body.receiver_key_epoch,
554            delivery_checkpoint_sequence: checkpoint_body.sequence,
555            delivery_checkpoint_digest: checkpoint.envelope_digest.clone(),
556            durable_blob_reference: checkpoint_body
557                .blob_reference
558                .clone()
559                .ok_or(OutcomeError::BindingMismatch)?,
560            durable_blob_digest: checkpoint_body
561                .blob_digest
562                .clone()
563                .ok_or(OutcomeError::BindingMismatch)?,
564        };
565        body.acknowledgement_id = body.derived_id()?;
566        body.validate()?;
567        Ok(body)
568    }
569
570    pub fn validate(&self) -> Result<(), OutcomeError> {
571        if self.schema != OUTCOME_DELIVERY_ACKNOWLEDGEMENT_SCHEMA {
572            return Err(OutcomeError::InvalidField("delivery_ack_schema"));
573        }
574        for (field, value) in [
575            ("acknowledgement_id", &self.acknowledgement_id),
576            ("eligibility_digest", &self.eligibility_digest),
577            (
578                "provider_acceptance_digest",
579                &self.provider_acceptance_digest,
580            ),
581            ("final_output_digest", &self.final_output_digest),
582            ("receiver_binding_digest", &self.receiver_binding_digest),
583            (
584                "delivery_checkpoint_digest",
585                &self.delivery_checkpoint_digest,
586            ),
587            ("durable_blob_digest", &self.durable_blob_digest),
588        ] {
589            validate_digest(field, value)?;
590        }
591        for (field, value) in [
592            ("request_id", &self.request_id),
593            ("delivery_id", &self.delivery_id),
594            ("idempotency_key", &self.idempotency_key),
595            ("receiver_queue_id", &self.receiver_queue_id),
596            ("receiver_key_id", &self.receiver_key_id),
597            ("durable_blob_reference", &self.durable_blob_reference),
598        ] {
599            validate_text(field, value)?;
600        }
601        for (field, value) in [
602            (
603                "delivery_accepted_at_unix_ms",
604                self.delivery_accepted_at_unix_ms,
605            ),
606            ("receiver_key_epoch", self.receiver_key_epoch),
607            (
608                "delivery_checkpoint_sequence",
609                self.delivery_checkpoint_sequence,
610            ),
611        ] {
612            validate_time(field, value)?;
613        }
614        if self.acknowledgement_id != self.derived_id()? {
615            return Err(OutcomeError::BindingMismatch);
616        }
617        if self.final_output_digest != self.durable_blob_digest {
618            return Err(OutcomeError::BindingMismatch);
619        }
620        Ok(())
621    }
622
623    fn derived_id(&self) -> Result<String, OutcomeError> {
624        domain_digest_without_field(ACK_ID_DOMAIN, self, "acknowledgementId")
625    }
626
627    #[must_use]
628    pub const fn delivery_accepted_at_unix_ms(&self) -> u64 {
629        self.delivery_accepted_at_unix_ms
630    }
631
632    #[must_use]
633    pub fn request_id(&self) -> &str {
634        &self.request_id
635    }
636
637    #[must_use]
638    pub fn eligibility_digest(&self) -> &str {
639        &self.eligibility_digest
640    }
641
642    #[must_use]
643    pub fn provider_acceptance_digest(&self) -> &str {
644        &self.provider_acceptance_digest
645    }
646
647    #[must_use]
648    pub fn final_output_digest(&self) -> &str {
649        &self.final_output_digest
650    }
651
652    #[must_use]
653    pub fn receiver_binding_digest(&self) -> &str {
654        &self.receiver_binding_digest
655    }
656}
657
658#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
659#[serde(transparent)]
660pub struct SignedOutcomeDeliveryAcknowledgementV1(
661    SignedExportEnvelope<OutcomeDeliveryAcknowledgementBodyV1>,
662);
663
664impl SignedOutcomeDeliveryAcknowledgementV1 {
665    pub fn sign(
666        body: OutcomeDeliveryAcknowledgementBodyV1,
667        signer: &Keypair,
668    ) -> Result<Self, OutcomeError> {
669        body.validate()?;
670        SignedExportEnvelope::sign(body, signer)
671            .map(Self)
672            .map_err(|error| OutcomeError::Canonicalization(error.to_string()))
673    }
674}
675
676#[derive(Debug, Clone)]
677pub struct AuthenticatedOutcomeDeliveryAcknowledgementV1 {
678    signed: SignedOutcomeDeliveryAcknowledgementV1,
679    envelope_digest: String,
680}
681
682impl AuthenticatedOutcomeDeliveryAcknowledgementV1 {
683    #[must_use]
684    pub fn envelope_digest(&self) -> &str {
685        &self.envelope_digest
686    }
687
688    #[must_use]
689    pub const fn body(&self) -> &OutcomeDeliveryAcknowledgementBodyV1 {
690        &self.signed.0.body
691    }
692}
693
694pub fn authenticate_outcome_delivery_acknowledgement(
695    canonical_envelope: &[u8],
696    eligibility: &AuthenticatedOutcomeEligibilityV1,
697    checkpoint: &AuthenticatedOutcomeDeliveryCheckpointV1,
698    trust: &OutcomeSignerTrustV1,
699) -> Result<AuthenticatedOutcomeDeliveryAcknowledgementV1, OutcomeError> {
700    let signed: SignedOutcomeDeliveryAcknowledgementV1 =
701        load_canonical_outcome_json(canonical_envelope)?;
702    signed.0.body.validate()?;
703    if trust.principal_id() != checkpoint.receiver_binding.receiver_key_id
704        || trust.key() != &checkpoint.receiver_binding.receiver_key
705        || trust.key_epoch() != checkpoint.receiver_binding.receiver_key_epoch
706    {
707        return Err(OutcomeError::BindingMismatch);
708    }
709    let expected = OutcomeDeliveryAcknowledgementBodyV1::from_receiver_assertion(
710        eligibility,
711        checkpoint,
712        OutcomeDeliveryAcknowledgementInputV1 {
713            receiver_key_id: trust.principal_id().to_owned(),
714        },
715    )?;
716    if signed.0.body != expected || signed.0.body.receiver_key_epoch != trust.key_epoch() {
717        return Err(OutcomeError::BindingMismatch);
718    }
719    if signed.0.signer_key != *trust.key()
720        || !signed
721            .0
722            .verify_signature()
723            .map_err(|error| OutcomeError::Canonicalization(error.to_string()))?
724    {
725        return Err(OutcomeError::AuthorityVerification);
726    }
727    if signed.0.body.delivery_accepted_at_unix_ms < eligibility.body().issued_at_unix_ms()
728        || signed.0.body.delivery_accepted_at_unix_ms
729            > eligibility.body().delivery_ack_deadline_unix_ms()
730        || signed.0.body.delivery_accepted_at_unix_ms >= eligibility.body().expires_at_unix_ms()
731        || signed.0.body.delivery_accepted_at_unix_ms
732            > eligibility.body().rail_capture_deadline_unix_ms()
733    {
734        return Err(OutcomeError::NotCurrent);
735    }
736    Ok(AuthenticatedOutcomeDeliveryAcknowledgementV1 {
737        envelope_digest: envelope_digest(&signed)?,
738        signed,
739    })
740}
741
742#[derive(Debug, Clone, PartialEq, Eq)]
743pub struct OutcomeDeliveryNonacceptanceInputV1 {
744    pub receiver_key_id: String,
745}
746
747#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
748#[serde(rename_all = "camelCase", deny_unknown_fields)]
749pub struct OutcomeDeliveryNonacceptanceBodyV1 {
750    schema: String,
751    nonacceptance_id: String,
752    request_id: String,
753    eligibility_digest: String,
754    provider_acceptance_digest: String,
755    output_digest: String,
756    receiver_binding_digest: String,
757    delivery_id: String,
758    idempotency_key: String,
759    receiver_queue_id: String,
760    cancelled_at_unix_ms: u64,
761    receiver_key_id: String,
762    receiver_key_epoch: u64,
763    delivery_checkpoint_sequence: u64,
764    delivery_checkpoint_digest: String,
765    blob_absence_proof_digest: String,
766    cancellation_fence_digest: String,
767}
768
769impl OutcomeDeliveryNonacceptanceBodyV1 {
770    pub fn from_receiver_assertion(
771        eligibility: &AuthenticatedOutcomeEligibilityV1,
772        checkpoint: &AuthenticatedOutcomeDeliveryCheckpointV1,
773        input: OutcomeDeliveryNonacceptanceInputV1,
774    ) -> Result<Self, OutcomeError> {
775        let checkpoint_body = checkpoint.body();
776        if checkpoint_body.state != OutcomeDeliveryCheckpointStateV1::Cancelled
777            || checkpoint_body.eligibility_digest != eligibility.envelope_digest()
778            || checkpoint_body.request_id != eligibility.body().request_id()
779            || checkpoint_body.receiver_binding_digest
780                != eligibility.body().receiver_binding_digest()
781        {
782            return Err(OutcomeError::BindingMismatch);
783        }
784        let mut body = Self {
785            schema: OUTCOME_DELIVERY_NONACCEPTANCE_SCHEMA.to_owned(),
786            nonacceptance_id: String::new(),
787            request_id: checkpoint_body.request_id.clone(),
788            eligibility_digest: checkpoint_body.eligibility_digest.clone(),
789            provider_acceptance_digest: checkpoint_body.provider_acceptance_digest.clone(),
790            output_digest: checkpoint_body.output_digest.clone(),
791            receiver_binding_digest: eligibility.body().receiver_binding_digest().to_owned(),
792            delivery_id: checkpoint_body.delivery_id.clone(),
793            idempotency_key: checkpoint_body.idempotency_key.clone(),
794            receiver_queue_id: checkpoint_body.receiver_queue_id.clone(),
795            cancelled_at_unix_ms: checkpoint_body.trusted_clock_high_water_unix_ms,
796            receiver_key_id: input.receiver_key_id,
797            receiver_key_epoch: checkpoint_body.receiver_key_epoch,
798            delivery_checkpoint_sequence: checkpoint_body.sequence,
799            delivery_checkpoint_digest: checkpoint.envelope_digest.clone(),
800            blob_absence_proof_digest: checkpoint_body
801                .blob_absence_proof_digest
802                .clone()
803                .ok_or(OutcomeError::BindingMismatch)?,
804            cancellation_fence_digest: checkpoint_body
805                .cancellation_fence_digest
806                .clone()
807                .ok_or(OutcomeError::BindingMismatch)?,
808        };
809        body.nonacceptance_id = body.derived_id()?;
810        body.validate()?;
811        Ok(body)
812    }
813
814    pub fn validate(&self) -> Result<(), OutcomeError> {
815        if self.schema != OUTCOME_DELIVERY_NONACCEPTANCE_SCHEMA {
816            return Err(OutcomeError::InvalidField("delivery_nonacceptance_schema"));
817        }
818        for (field, value) in [
819            ("nonacceptance_id", &self.nonacceptance_id),
820            ("eligibility_digest", &self.eligibility_digest),
821            (
822                "provider_acceptance_digest",
823                &self.provider_acceptance_digest,
824            ),
825            ("output_digest", &self.output_digest),
826            ("receiver_binding_digest", &self.receiver_binding_digest),
827            (
828                "delivery_checkpoint_digest",
829                &self.delivery_checkpoint_digest,
830            ),
831            ("blob_absence_proof_digest", &self.blob_absence_proof_digest),
832            ("cancellation_fence_digest", &self.cancellation_fence_digest),
833        ] {
834            validate_digest(field, value)?;
835        }
836        for (field, value) in [
837            ("request_id", &self.request_id),
838            ("delivery_id", &self.delivery_id),
839            ("idempotency_key", &self.idempotency_key),
840            ("receiver_queue_id", &self.receiver_queue_id),
841            ("receiver_key_id", &self.receiver_key_id),
842        ] {
843            validate_text(field, value)?;
844        }
845        for (field, value) in [
846            ("cancelled_at_unix_ms", self.cancelled_at_unix_ms),
847            ("receiver_key_epoch", self.receiver_key_epoch),
848            (
849                "delivery_checkpoint_sequence",
850                self.delivery_checkpoint_sequence,
851            ),
852        ] {
853            validate_time(field, value)?;
854        }
855        if self.nonacceptance_id != self.derived_id()? {
856            return Err(OutcomeError::BindingMismatch);
857        }
858        Ok(())
859    }
860
861    fn derived_id(&self) -> Result<String, OutcomeError> {
862        domain_digest_without_field(NONACCEPTANCE_ID_DOMAIN, self, "nonacceptanceId")
863    }
864
865    #[must_use]
866    pub const fn cancelled_at_unix_ms(&self) -> u64 {
867        self.cancelled_at_unix_ms
868    }
869
870    #[must_use]
871    pub fn request_id(&self) -> &str {
872        &self.request_id
873    }
874
875    #[must_use]
876    pub fn eligibility_digest(&self) -> &str {
877        &self.eligibility_digest
878    }
879
880    #[must_use]
881    pub fn provider_acceptance_digest(&self) -> &str {
882        &self.provider_acceptance_digest
883    }
884
885    #[must_use]
886    pub fn output_digest(&self) -> &str {
887        &self.output_digest
888    }
889
890    #[must_use]
891    pub fn receiver_binding_digest(&self) -> &str {
892        &self.receiver_binding_digest
893    }
894}
895
896#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
897#[serde(transparent)]
898pub struct SignedOutcomeDeliveryNonacceptanceV1(
899    SignedExportEnvelope<OutcomeDeliveryNonacceptanceBodyV1>,
900);
901
902impl SignedOutcomeDeliveryNonacceptanceV1 {
903    pub fn sign(
904        body: OutcomeDeliveryNonacceptanceBodyV1,
905        signer: &Keypair,
906    ) -> Result<Self, OutcomeError> {
907        body.validate()?;
908        SignedExportEnvelope::sign(body, signer)
909            .map(Self)
910            .map_err(|error| OutcomeError::Canonicalization(error.to_string()))
911    }
912}
913
914#[derive(Debug, Clone)]
915pub struct AuthenticatedOutcomeDeliveryNonacceptanceV1 {
916    signed: SignedOutcomeDeliveryNonacceptanceV1,
917    envelope_digest: String,
918}
919
920impl AuthenticatedOutcomeDeliveryNonacceptanceV1 {
921    #[must_use]
922    pub fn envelope_digest(&self) -> &str {
923        &self.envelope_digest
924    }
925
926    #[must_use]
927    pub const fn body(&self) -> &OutcomeDeliveryNonacceptanceBodyV1 {
928        &self.signed.0.body
929    }
930}
931
932pub fn authenticate_outcome_delivery_nonacceptance(
933    canonical_envelope: &[u8],
934    eligibility: &AuthenticatedOutcomeEligibilityV1,
935    checkpoint: &AuthenticatedOutcomeDeliveryCheckpointV1,
936    trust: &OutcomeSignerTrustV1,
937) -> Result<AuthenticatedOutcomeDeliveryNonacceptanceV1, OutcomeError> {
938    let signed: SignedOutcomeDeliveryNonacceptanceV1 =
939        load_canonical_outcome_json(canonical_envelope)?;
940    signed.0.body.validate()?;
941    if trust.principal_id() != checkpoint.receiver_binding.receiver_key_id
942        || trust.key() != &checkpoint.receiver_binding.receiver_key
943        || trust.key_epoch() != checkpoint.receiver_binding.receiver_key_epoch
944    {
945        return Err(OutcomeError::BindingMismatch);
946    }
947    let expected = OutcomeDeliveryNonacceptanceBodyV1::from_receiver_assertion(
948        eligibility,
949        checkpoint,
950        OutcomeDeliveryNonacceptanceInputV1 {
951            receiver_key_id: trust.principal_id().to_owned(),
952        },
953    )?;
954    if signed.0.body != expected || signed.0.body.receiver_key_epoch != trust.key_epoch() {
955        return Err(OutcomeError::BindingMismatch);
956    }
957    if signed.0.signer_key != *trust.key()
958        || !signed
959            .0
960            .verify_signature()
961            .map_err(|error| OutcomeError::Canonicalization(error.to_string()))?
962    {
963        return Err(OutcomeError::AuthorityVerification);
964    }
965    if signed.0.body.cancelled_at_unix_ms < eligibility.body().issued_at_unix_ms() {
966        return Err(OutcomeError::NotCurrent);
967    }
968    Ok(AuthenticatedOutcomeDeliveryNonacceptanceV1 {
969        envelope_digest: envelope_digest(&signed)?,
970        signed,
971    })
972}