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}