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}