Skip to main content

chio_settle/channel/
reservation.rs

1use chio_core::capability::scope::MonetaryAmount;
2use chio_core::crypto::PublicKey;
3use chio_core::economic_continuity::{
4    EconomicAdmissionHandoffStateV1, EconomicContentV1, EconomicEffectSlotV1,
5    EconomicEffectStateV1, EconomicRequestReplayV1, EconomicResourceHeadV1, EconomicResourceKeyV1,
6    VerifiedEconomicStateView,
7};
8use serde::{Deserialize, Serialize};
9
10use super::state::next_sequence;
11use super::validation::{
12    digest, parse_base_units, validate_currency, validate_digest, validate_positive, validate_text,
13};
14use super::{
15    ChannelError, ChannelEscrowReservationStatusV1, ChannelLifecycleStatusV1, ChannelOpenTrustV1,
16    ChannelSignatureV1, VerifiedAdmittedChannelOpenV1, VerifiedChannelLifecycleSnapshotV1,
17    VerifiedChannelStateV1, CHANNEL_LIFECYCLE_RESOURCE_FAMILY,
18    CHANNEL_SERVICE_DISPATCH_EFFECT_KIND,
19};
20
21pub const CHANNEL_RESERVATION_SCHEMA: &str = "chio.channel.reservation.v1";
22
23const CHANNEL_RESERVATION_ID_DOMAIN: &[u8] = b"chio.channel.reservation.id.v1\0";
24const CHANNEL_RESERVATION_PROPOSAL_DIGEST_DOMAIN: &[u8] =
25    b"chio.channel.reservation-proposal.digest.v1\0";
26const CHANNEL_RESERVATION_DIGEST_DOMAIN: &[u8] = b"chio.channel.reservation.digest.v1\0";
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase", deny_unknown_fields)]
30pub struct ChannelReservationBodyV1 {
31    pub schema: String,
32    pub reservation_id: String,
33    pub channel_id: String,
34    pub open_digest: String,
35    pub request_id: String,
36    pub operation_id: String,
37    pub next_sequence: u64,
38    pub prior_state_digest: String,
39    pub service_binding_digest: String,
40    pub receipt_authority_digest: String,
41    pub maximum_charge: MonetaryAmount,
42    pub maximum_token_base_units: String,
43    pub expires_at_unix_ms: u64,
44    pub disposition_expected_version: u64,
45    pub channel_state_expected_version: u64,
46    pub lifecycle_fence: u64,
47}
48
49impl ChannelReservationBodyV1 {
50    pub fn validate(&self) -> Result<(), ChannelError> {
51        if self.schema != CHANNEL_RESERVATION_SCHEMA {
52            return Err(ChannelError::InvalidField("channel_reservation_schema"));
53        }
54        for (field, value) in [
55            ("reservation_id", &self.reservation_id),
56            ("reservation_channel_id", &self.channel_id),
57            ("reservation_open_digest", &self.open_digest),
58            ("reservation_prior_state_digest", &self.prior_state_digest),
59            (
60                "reservation_service_binding_digest",
61                &self.service_binding_digest,
62            ),
63            (
64                "reservation_receipt_authority_digest",
65                &self.receipt_authority_digest,
66            ),
67        ] {
68            validate_digest(field, value)?;
69        }
70        validate_text("reservation_request_id", &self.request_id)?;
71        validate_digest("reservation_operation_id", &self.operation_id)?;
72        validate_currency(&self.maximum_charge.currency)?;
73        for (field, value) in [
74            ("reservation_next_sequence", self.next_sequence),
75            ("reservation_maximum_charge", self.maximum_charge.units),
76            ("reservation_expiry", self.expires_at_unix_ms),
77            (
78                "reservation_disposition_version",
79                self.disposition_expected_version,
80            ),
81            (
82                "reservation_channel_state_version",
83                self.channel_state_expected_version,
84            ),
85            ("reservation_lifecycle_fence", self.lifecycle_fence),
86        ] {
87            validate_positive(field, value)?;
88        }
89        if parse_base_units(&self.maximum_token_base_units)? == 0 {
90            return Err(ChannelError::InvalidField(
91                "reservation_maximum_token_base_units",
92            ));
93        }
94        if self.disposition_expected_version != 1 {
95            return Err(ChannelError::InvalidField(
96                "reservation_disposition_version",
97            ));
98        }
99        Ok(())
100    }
101
102    pub fn proposal_digest(&self) -> Result<String, ChannelError> {
103        self.validate()?;
104        digest(CHANNEL_RESERVATION_PROPOSAL_DIGEST_DOMAIN, self)
105    }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "camelCase", deny_unknown_fields)]
110pub struct SignedChannelReservationV1 {
111    pub body: ChannelReservationBodyV1,
112    pub payer_signature: ChannelSignatureV1,
113    pub authority_signature: ChannelSignatureV1,
114}
115
116impl SignedChannelReservationV1 {
117    pub fn digest(&self) -> Result<String, ChannelError> {
118        self.body.validate()?;
119        digest(CHANNEL_RESERVATION_DIGEST_DOMAIN, self)
120    }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct VerifiedChannelReservationProposalV1 {
125    reservation: SignedChannelReservationV1,
126    accepted_at_unix_ms: u64,
127    settlement_authority_scope_id: String,
128    escrow_reference: super::ChannelEscrowReferenceV1,
129}
130
131impl VerifiedChannelReservationProposalV1 {
132    #[must_use]
133    pub const fn artifact(&self) -> &SignedChannelReservationV1 {
134        &self.reservation
135    }
136
137    #[must_use]
138    pub const fn accepted_at_unix_ms(&self) -> u64 {
139        self.accepted_at_unix_ms
140    }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase", deny_unknown_fields)]
145pub struct ChannelReservationAuthorityV1 {
146    pub authority_id: String,
147    pub authority_key_epoch: u64,
148    #[serde(deserialize_with = "super::signed::deserialize_canonical_public_key")]
149    pub authority_key: PublicKey,
150    pub trusted_time_unix_ms: u64,
151}
152
153impl ChannelReservationAuthorityV1 {
154    pub(super) fn validate(&self) -> Result<(), ChannelError> {
155        validate_text("channel_authority_id", &self.authority_id)?;
156        validate_positive("channel_authority_key_epoch", self.authority_key_epoch)?;
157        validate_positive("channel_authority_trusted_time", self.trusted_time_unix_ms)
158    }
159}
160
161pub fn derive_channel_reservation_id(
162    channel_id: &str,
163    open_digest: &str,
164    request_id: &str,
165    next_sequence: u64,
166    prior_state_digest: &str,
167) -> Result<String, ChannelError> {
168    validate_digest("reservation_channel_id", channel_id)?;
169    validate_digest("reservation_open_digest", open_digest)?;
170    validate_text("reservation_request_id", request_id)?;
171    validate_positive("reservation_next_sequence", next_sequence)?;
172    validate_digest("reservation_prior_state_digest", prior_state_digest)?;
173    digest(
174        CHANNEL_RESERVATION_ID_DOMAIN,
175        &(
176            channel_id,
177            open_digest,
178            request_id,
179            next_sequence,
180            prior_state_digest,
181        ),
182    )
183}
184
185pub fn verify_channel_reservation_proposal(
186    reservation: &SignedChannelReservationV1,
187    open: &VerifiedAdmittedChannelOpenV1,
188    prior: &VerifiedChannelStateV1,
189    authority: &ChannelReservationAuthorityV1,
190    trust: &ChannelOpenTrustV1,
191) -> Result<VerifiedChannelReservationProposalV1, ChannelError> {
192    let lifecycle = open.snapshot().lifecycle();
193    let open = open.consent();
194    let intent = open.intent();
195    let open_artifact = open.artifact();
196    let prior_digest = prior.digest()?;
197    let prior = prior.body();
198    reservation.body.validate()?;
199    prior.validate()?;
200    lifecycle.validate()?;
201    authority.validate()?;
202    trust.validate()?;
203    let body = &reservation.body;
204    reservation.payer_signature.verify(
205        body,
206        &trust.payer_id,
207        trust.payer_key_epoch,
208        &trust.payer_key,
209    )?;
210    reservation.authority_signature.verify(
211        body,
212        &authority.authority_id,
213        authority.authority_key_epoch,
214        &authority.authority_key,
215    )?;
216    let open_digest = open_artifact.digest()?;
217    let expected_reservation_id = derive_channel_reservation_id(
218        &body.channel_id,
219        &open_digest,
220        &body.request_id,
221        body.next_sequence,
222        &prior_digest,
223    )?;
224    let expected_sequence = next_sequence(prior.seq)?;
225    let expected_asset_digest = intent.body.asset_binding.digest()?;
226    let remaining = intent
227        .body
228        .bound
229        .units
230        .checked_sub(prior.cumulative_owed.units)
231        .ok_or(ChannelError::ArithmeticOverflow)?;
232    let channel_expiry_ms = intent
233        .body
234        .channel_expiry_unix_secs
235        .checked_mul(1_000)
236        .ok_or(ChannelError::ArithmeticOverflow)?;
237    intent
238        .body
239        .asset_binding
240        .verify_round_trip(&body.maximum_charge, &body.maximum_token_base_units)?;
241    intent
242        .body
243        .asset_binding
244        .verify_round_trip(&prior.cumulative_owed, &prior.cumulative_token_base_units)?;
245    if body.reservation_id != expected_reservation_id
246        || body.channel_id != open_artifact.body.channel_id
247        || body.channel_id != prior.channel_id
248        || body.open_digest != open_digest
249        || open_artifact.body.open_intent_digest != intent.digest()?
250        || body.next_sequence != expected_sequence
251        || body.prior_state_digest != prior_digest
252        || body.maximum_charge.currency != intent.body.currency
253        || body.maximum_charge.units > remaining
254        || parse_base_units(&body.maximum_token_base_units)?
255            > parse_base_units(&intent.body.bound_token_base_units)?
256        || prior.cumulative_owed.currency != intent.body.currency
257        || prior.asset_binding_digest != expected_asset_digest
258        || !trust.matches_intent(&intent.body)
259        || lifecycle.status != ChannelLifecycleStatusV1::Open
260        || lifecycle.channel_id != body.channel_id
261        || lifecycle.latest_state_digest != prior_digest
262        || lifecycle.latest_sequence != prior.seq
263        || lifecycle.state_version != body.channel_state_expected_version
264        || lifecycle.lifecycle_fence != body.lifecycle_fence
265        || lifecycle.live_reservation_id.is_some()
266        || lifecycle.operation_id.is_some()
267        || body.expires_at_unix_ms <= authority.trusted_time_unix_ms
268        || body.expires_at_unix_ms <= trust.trusted_time_unix_ms
269        || body.expires_at_unix_ms > channel_expiry_ms
270        || authority.trusted_time_unix_ms != trust.trusted_time_unix_ms
271        || prior.seq == 0 && open_artifact.body.initial_state_digest != prior_digest
272    {
273        return Err(ChannelError::AuthorityVerification);
274    }
275    Ok(VerifiedChannelReservationProposalV1 {
276        reservation: reservation.clone(),
277        accepted_at_unix_ms: authority.trusted_time_unix_ms,
278        settlement_authority_scope_id: intent.body.settlement_authority_scope_id.clone(),
279        escrow_reference: intent.body.escrow_reference.clone(),
280    })
281}
282
283#[derive(Debug, Clone)]
284pub struct VerifiedAdmittedChannelReservationV1 {
285    proposal: VerifiedChannelReservationProposalV1,
286    snapshot: VerifiedChannelLifecycleSnapshotV1,
287    ready_effect: EconomicEffectSlotV1,
288    ready_effect_head_digest: String,
289}
290
291impl VerifiedAdmittedChannelReservationV1 {
292    #[must_use]
293    pub const fn proposal(&self) -> &VerifiedChannelReservationProposalV1 {
294        &self.proposal
295    }
296
297    #[must_use]
298    pub const fn snapshot(&self) -> &VerifiedChannelLifecycleSnapshotV1 {
299        &self.snapshot
300    }
301
302    #[must_use]
303    pub const fn artifact(&self) -> &SignedChannelReservationV1 {
304        self.proposal.artifact()
305    }
306
307    #[must_use]
308    pub const fn accepted_at_unix_ms(&self) -> u64 {
309        self.proposal.accepted_at_unix_ms()
310    }
311
312    #[must_use]
313    pub const fn ready_effect(&self) -> &EconomicEffectSlotV1 {
314        &self.ready_effect
315    }
316
317    #[must_use]
318    pub fn ready_effect_head_digest(&self) -> &str {
319        &self.ready_effect_head_digest
320    }
321}
322
323pub fn verify_admitted_channel_reservation(
324    proposal: &VerifiedChannelReservationProposalV1,
325    prepared: &super::VerifiedChannelPreparedReservationV1,
326    current: &VerifiedEconomicStateView,
327) -> Result<VerifiedAdmittedChannelReservationV1, ChannelError> {
328    let body = &proposal.artifact().body;
329    let prepared_plan = prepared.prepared();
330    let service = &prepared_plan.service;
331    let prepared_current = prepared.current();
332    let snapshot = super::verify_channel_lifecycle_snapshot(
333        current,
334        &proposal.settlement_authority_scope_id,
335        &body.channel_id,
336    )?;
337    let lifecycle = snapshot.lifecycle();
338    let escrow = snapshot.escrow();
339    let expected_state_version = body
340        .channel_state_expected_version
341        .checked_add(1)
342        .ok_or(ChannelError::ArithmeticOverflow)?;
343    let expected_fence = body
344        .lifecycle_fence
345        .checked_add(1)
346        .ok_or(ChannelError::ArithmeticOverflow)?;
347    let prior_sequence = body
348        .next_sequence
349        .checked_sub(1)
350        .ok_or(ChannelError::ArithmeticOverflow)?;
351    let ready_effect_head = exact_ready_effect_head(current, &body.operation_id)?;
352    let ready_effect = decode_effect_head(ready_effect_head)?;
353    ready_effect
354        .validate()
355        .map_err(|_| ChannelError::AuthorityVerification)?;
356    let reservation_digest = proposal.artifact().digest()?;
357    let expected_idempotency_key = super::derive_channel_service_dispatch_idempotency_key(
358        &body.operation_id,
359        &body.reservation_id,
360        body.next_sequence,
361    )?;
362    let channel_key = EconomicResourceKeyV1 {
363        resource_family: CHANNEL_LIFECYCLE_RESOURCE_FAMILY.to_owned(),
364        scope_id: proposal.settlement_authority_scope_id.clone(),
365        resource_id: body.channel_id.clone(),
366    };
367    let ready_effect_head_digest = ready_effect_head
368        .digest()
369        .map_err(|_| ChannelError::AuthorityVerification)?;
370    let expected_replay = EconomicRequestReplayV1 {
371        request: service.request.clone(),
372        operation_id: body.operation_id.clone(),
373        effect_slot_ids: vec![ready_effect.slot_id.clone()],
374    };
375    expected_replay
376        .validate()
377        .map_err(|_| ChannelError::AuthorityVerification)?;
378    let retained_replay = current
379        .view()
380        .request_replay(&service.request.key())
381        .ok_or(ChannelError::AuthorityVerification)?;
382    let prepared_snapshot = super::verify_channel_lifecycle_snapshot(
383        prepared_current,
384        &proposal.settlement_authority_scope_id,
385        &body.channel_id,
386    )?;
387    let expected_channel_head_version = prepared_snapshot
388        .channel_head()
389        .head_version
390        .checked_add(1)
391        .ok_or(ChannelError::ArithmeticOverflow)?;
392    let expected_escrow_head_version = prepared_snapshot
393        .escrow_head()
394        .head_version
395        .checked_add(1)
396        .ok_or(ChannelError::ArithmeticOverflow)?;
397    let authored_at_unix_ms = snapshot.channel_head().trusted_clock_high_water;
398    if lifecycle.status != ChannelLifecycleStatusV1::Open
399        || &prepared_plan.reservation != body
400        || prepared_plan.signed_open.body.channel_id != body.channel_id
401        || body.service_binding_digest != service.digest()?
402        || current.view().checkpoint_sequence <= prepared_plan.checkpoint_sequence
403        || current.view().checkpoint_digest == prepared_plan.checkpoint_digest
404        || current.view().observed_at < prepared_plan.observed_at_unix_ms
405        || prepared_snapshot.channel_head_digest() != prepared_plan.channel_head_digest
406        || prepared_snapshot.escrow_head_digest() != prepared_plan.escrow_head_digest
407        || snapshot.channel_head().predecessor_digest.as_deref()
408            != Some(prepared_plan.channel_head_digest.as_str())
409        || snapshot.escrow_head().predecessor_digest.as_deref()
410            != Some(prepared_plan.escrow_head_digest.as_str())
411        || snapshot.channel_head().head_version != expected_channel_head_version
412        || snapshot.escrow_head().head_version != expected_escrow_head_version
413        || lifecycle.latest_state_digest != body.prior_state_digest
414        || lifecycle.latest_sequence != prior_sequence
415        || lifecycle.state_version != expected_state_version
416        || lifecycle.lifecycle_fence != expected_fence
417        || lifecycle.live_reservation_id.as_deref() != Some(&body.reservation_id)
418        || lifecycle.operation_id.as_deref() != Some(&body.operation_id)
419        || escrow.status != ChannelEscrowReservationStatusV1::Open
420        || escrow.open_digest != body.open_digest
421        || escrow.escrow_reference != proposal.escrow_reference
422        || snapshot.escrow_head().trusted_clock_high_water != authored_at_unix_ms
423        || ready_effect_head.trusted_clock_high_water != authored_at_unix_ms
424        || authored_at_unix_ms < proposal.accepted_at_unix_ms
425        || authored_at_unix_ms < prepared_plan.observed_at_unix_ms
426        || authored_at_unix_ms > snapshot.observed_at_unix_ms()
427        || snapshot.observed_at_unix_ms() < proposal.accepted_at_unix_ms
428        || snapshot.observed_at_unix_ms() >= body.expires_at_unix_ms
429        || ready_effect.anchor_id != current.view().anchor_id
430        || ready_effect.namespace != current.view().namespace
431        || ready_effect.operation_id != body.operation_id
432        || ready_effect.effect_kind != CHANNEL_SERVICE_DISPATCH_EFFECT_KIND
433        || ready_effect.request != service.request
434        || ready_effect.request.request_id != body.request_id
435        || ready_effect.admission_handoff != service.admission_handoff
436        || ready_effect.target != service.provider
437        || ready_effect.action_digest != service.action_digest
438        || ready_effect.resource_key != channel_key
439        || ready_effect.resource_head_digest != snapshot.channel_head_digest()
440        || ready_effect.admission_handoff.state
441            != EconomicAdmissionHandoffStateV1::DispatchCommitted
442        || ready_effect.parameters_digest != reservation_digest
443        || ready_effect.idempotency_key != expected_idempotency_key
444        || ready_effect.frost.is_some()
445        || ready_effect.state != EconomicEffectStateV1::Ready
446        || ready_effect.terminal.is_some()
447        || ready_effect_head.resource_key != ready_effect.resource_head_key()
448        || ready_effect_head.head_version != 1
449        || ready_effect_head.resource_version != 1
450        || ready_effect_head.lifecycle_fence != 1
451        || ready_effect_head.lifecycle_state != "ready"
452        || ready_effect_head.operation_id.as_deref() != Some(body.operation_id.as_str())
453        || ready_effect_head.effect_idempotency_key.as_deref()
454            != Some(expected_idempotency_key.as_str())
455        || ready_effect_head.frost.is_some()
456        || ready_effect_head.terminal_result.is_some()
457        || ready_effect_head.predecessor_digest.is_some()
458        || retained_replay != &expected_replay
459    {
460        return Err(ChannelError::AuthorityVerification);
461    }
462    Ok(VerifiedAdmittedChannelReservationV1 {
463        proposal: proposal.clone(),
464        snapshot,
465        ready_effect,
466        ready_effect_head_digest,
467    })
468}
469
470fn exact_ready_effect_head<'a>(
471    current: &'a VerifiedEconomicStateView,
472    operation_id: &str,
473) -> Result<&'a EconomicResourceHeadV1, ChannelError> {
474    let mut matches = current.view().heads.iter().filter(|head| {
475        head.resource_key.resource_family == "effect_slot"
476            && head.operation_id.as_deref() == Some(operation_id)
477    });
478    let head = matches.next().ok_or(ChannelError::AuthorityVerification)?;
479    if matches.next().is_some() {
480        return Err(ChannelError::AuthorityVerification);
481    }
482    Ok(head)
483}
484
485fn decode_effect_head(head: &EconomicResourceHeadV1) -> Result<EconomicEffectSlotV1, ChannelError> {
486    let EconomicContentV1::Inline { value } = &head.state else {
487        return Err(ChannelError::AuthorityVerification);
488    };
489    serde_json::from_value(value.clone()).map_err(|_| ChannelError::AuthorityVerification)
490}
491
492#[cfg(test)]
493mod proposal_digest_tests {
494    use super::*;
495
496    fn body() -> ChannelReservationBodyV1 {
497        ChannelReservationBodyV1 {
498            schema: CHANNEL_RESERVATION_SCHEMA.to_owned(),
499            reservation_id: "11".repeat(32),
500            channel_id: "22".repeat(32),
501            open_digest: "33".repeat(32),
502            request_id: "request-1".to_owned(),
503            operation_id: "44".repeat(32),
504            next_sequence: 1,
505            prior_state_digest: "55".repeat(32),
506            service_binding_digest: "66".repeat(32),
507            receipt_authority_digest: "77".repeat(32),
508            maximum_charge: MonetaryAmount {
509                units: 10,
510                currency: "USD".to_owned(),
511            },
512            maximum_token_base_units: "10000000".to_owned(),
513            expires_at_unix_ms: 2_000,
514            disposition_expected_version: 1,
515            channel_state_expected_version: 1,
516            lifecycle_fence: 2,
517        }
518    }
519
520    #[test]
521    fn reservation_proposal_digest_binds_the_unsigned_body() -> Result<(), ChannelError> {
522        let proposal = body();
523        let digest = proposal.proposal_digest()?;
524        let mut substituted = proposal;
525        substituted.maximum_charge.units += 1;
526
527        assert_ne!(digest, substituted.proposal_digest()?);
528        Ok(())
529    }
530}