1use chio_core::economic_continuity::{
2 EconomicActionAuthorizationV1, EconomicAdmissionHandoffStateV1, EconomicAdmissionHandoffV1,
3 EconomicContentV1, EconomicEffectSlotV1, EconomicEffectStateV1, EconomicEffectTargetV1,
4 EconomicPreparedEffectV1, EconomicRequestBindingV1, EconomicRequestReplayV1,
5 EconomicResourceHeadV1, EconomicResourceKeyV1, EconomicStateTransitionV1,
6 CHIO_ECONOMIC_EFFECT_SLOT_SCHEMA, CHIO_ECONOMIC_RESOURCE_HEAD_SCHEMA,
7};
8use serde::{Deserialize, Serialize};
9
10use super::super::state::next_sequence;
11use super::super::validation::{
12 digest, parse_base_units, validate_digest, validate_positive, validate_text,
13};
14use super::super::{
15 derive_channel_reservation_id, derive_channel_service_dispatch_idempotency_key,
16 verify_channel_lifecycle_snapshot, ChannelError, ChannelEscrowReservationStatusV1,
17 ChannelEscrowReservationViewV1, ChannelLifecycleStatusV1, ChannelLifecycleViewV1,
18 ChannelReservationBodyV1, ChannelStateBodyV1, SignedChannelOpenIntentV1, SignedChannelOpenV1,
19 SignedChannelStateV1, VerifiedAdmittedChannelOpenV1, VerifiedChannelReservationProposalV1,
20 VerifiedChannelStateV1, CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY,
21 CHANNEL_LIFECYCLE_RESOURCE_FAMILY, CHANNEL_SERVICE_DISPATCH_EFFECT_KIND,
22};
23use super::{
24 head_digest, successor_head, transition, ChannelLifecycleProjectionV1, SuccessorHeadBinding,
25};
26
27pub const CHANNEL_PREPARED_RESERVATION_SCHEMA: &str = "chio.channel.prepared-reservation.v1";
28
29const CHANNEL_SERVICE_BINDING_DIGEST_DOMAIN: &[u8] = b"chio.channel.service-binding.digest.v1\0";
30const CHANNEL_PREPARED_RESERVATION_DIGEST_DOMAIN: &[u8] =
31 b"chio.channel.prepared-reservation.digest.v1\0";
32const CHANNEL_RESERVATION_TRANSITION_PROOF_SCHEMA: &str =
33 "chio.channel.reservation-transition-proof.v1";
34const CHANNEL_RESERVATION_TRANSITION_PROOF_DOMAIN: &[u8] =
35 b"chio.channel.reservation-transition-proof.digest.v1\0";
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase", deny_unknown_fields)]
39pub struct ChannelServiceBindingV1 {
40 pub request: EconomicRequestBindingV1,
41 pub admission_handoff: EconomicAdmissionHandoffV1,
42 pub provider: EconomicEffectTargetV1,
43 pub action_digest: String,
44}
45
46impl ChannelServiceBindingV1 {
47 fn validate(&self) -> Result<(), ChannelError> {
48 self.request
49 .validate()
50 .map_err(|_| ChannelError::AuthorityVerification)?;
51 self.admission_handoff
52 .validate()
53 .map_err(|_| ChannelError::AuthorityVerification)?;
54 self.provider
55 .validate()
56 .map_err(|_| ChannelError::AuthorityVerification)?;
57 validate_digest("channel_service_action_digest", &self.action_digest)?;
58 if self.admission_handoff.state != EconomicAdmissionHandoffStateV1::DispatchCommitted {
59 return Err(ChannelError::AuthorityVerification);
60 }
61 Ok(())
62 }
63
64 pub fn digest(&self) -> Result<String, ChannelError> {
65 self.validate()?;
66 digest(CHANNEL_SERVICE_BINDING_DIGEST_DOMAIN, self)
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(tag = "stateKind", rename_all = "snake_case", deny_unknown_fields)]
72pub enum RetainedChannelStateV1 {
73 Initial { body: Box<ChannelStateBodyV1> },
74 Signed { state: Box<SignedChannelStateV1> },
75}
76
77impl RetainedChannelStateV1 {
78 pub(super) fn from_verified(state: &VerifiedChannelStateV1) -> Self {
79 match state.payee_signature() {
80 Some(payee_signature) => Self::Signed {
81 state: Box::new(SignedChannelStateV1 {
82 body: state.body().clone(),
83 payee_signature: payee_signature.clone(),
84 }),
85 },
86 None => Self::Initial {
87 body: Box::new(state.body().clone()),
88 },
89 }
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase", deny_unknown_fields)]
95pub struct ChannelPreparedReservationV1 {
96 pub schema: String,
97 pub signed_open_intent: SignedChannelOpenIntentV1,
98 pub signed_open: SignedChannelOpenV1,
99 pub prior_state: RetainedChannelStateV1,
100 pub reservation: ChannelReservationBodyV1,
101 pub service: ChannelServiceBindingV1,
102 pub lifecycle: ChannelLifecycleViewV1,
103 pub escrow: ChannelEscrowReservationViewV1,
104 pub channel_head_digest: String,
105 pub escrow_head_digest: String,
106 pub anchor_id: String,
107 pub namespace: String,
108 pub checkpoint_sequence: u64,
109 pub checkpoint_digest: String,
110 pub observed_at_unix_ms: u64,
111}
112
113impl ChannelPreparedReservationV1 {
114 #[must_use]
115 pub const fn reservation(&self) -> &ChannelReservationBodyV1 {
116 &self.reservation
117 }
118
119 #[must_use]
120 pub const fn service(&self) -> &ChannelServiceBindingV1 {
121 &self.service
122 }
123
124 pub fn digest(&self) -> Result<String, ChannelError> {
125 if self.schema != CHANNEL_PREPARED_RESERVATION_SCHEMA {
126 return Err(ChannelError::InvalidField(
127 "channel_prepared_reservation_schema",
128 ));
129 }
130 self.signed_open_intent.digest()?;
131 self.signed_open.digest()?;
132 match &self.prior_state {
133 RetainedChannelStateV1::Initial { body } => body.validate()?,
134 RetainedChannelStateV1::Signed { state } => {
135 state.digest()?;
136 }
137 }
138 self.reservation.validate()?;
139 self.service.validate()?;
140 self.lifecycle.validate()?;
141 self.escrow.validate()?;
142 validate_digest(
143 "channel_prepared_channel_head_digest",
144 &self.channel_head_digest,
145 )?;
146 validate_digest(
147 "channel_prepared_escrow_head_digest",
148 &self.escrow_head_digest,
149 )?;
150 validate_text("channel_prepared_anchor_id", &self.anchor_id)?;
151 validate_text("channel_prepared_namespace", &self.namespace)?;
152 validate_positive(
153 "channel_prepared_checkpoint_sequence",
154 self.checkpoint_sequence,
155 )?;
156 validate_digest(
157 "channel_prepared_checkpoint_digest",
158 &self.checkpoint_digest,
159 )?;
160 validate_positive("channel_prepared_observed_at", self.observed_at_unix_ms)?;
161 digest(CHANNEL_PREPARED_RESERVATION_DIGEST_DOMAIN, self)
162 }
163}
164
165#[derive(Debug, Clone)]
166pub struct VerifiedChannelPreparedReservationV1 {
167 prepared: ChannelPreparedReservationV1,
168 open: VerifiedAdmittedChannelOpenV1,
169 prior: VerifiedChannelStateV1,
170 current: chio_core::economic_continuity::VerifiedEconomicStateView,
171}
172
173impl VerifiedChannelPreparedReservationV1 {
174 #[must_use]
175 pub const fn prepared(&self) -> &ChannelPreparedReservationV1 {
176 &self.prepared
177 }
178
179 pub(crate) const fn current(
180 &self,
181 ) -> &chio_core::economic_continuity::VerifiedEconomicStateView {
182 &self.current
183 }
184}
185
186pub fn prepare_channel_reservation(
187 open: &VerifiedAdmittedChannelOpenV1,
188 prior: &VerifiedChannelStateV1,
189 current: &chio_core::economic_continuity::VerifiedEconomicStateView,
190 reservation: ChannelReservationBodyV1,
191 service: ChannelServiceBindingV1,
192) -> Result<ChannelPreparedReservationV1, ChannelError> {
193 let snapshot = verify_channel_lifecycle_snapshot(
194 current,
195 &open.consent().intent().body.settlement_authority_scope_id,
196 &open.consent().artifact().body.channel_id,
197 )?;
198 let prepared = ChannelPreparedReservationV1 {
199 schema: CHANNEL_PREPARED_RESERVATION_SCHEMA.to_owned(),
200 signed_open_intent: open.consent().intent().clone(),
201 signed_open: open.consent().artifact().clone(),
202 prior_state: RetainedChannelStateV1::from_verified(prior),
203 reservation,
204 service,
205 lifecycle: snapshot.lifecycle().clone(),
206 escrow: snapshot.escrow().clone(),
207 channel_head_digest: snapshot.channel_head_digest().to_owned(),
208 escrow_head_digest: snapshot.escrow_head_digest().to_owned(),
209 anchor_id: current.view().anchor_id.clone(),
210 namespace: current.view().namespace.clone(),
211 checkpoint_sequence: current.view().checkpoint_sequence,
212 checkpoint_digest: current.view().checkpoint_digest.clone(),
213 observed_at_unix_ms: current.view().observed_at,
214 };
215 validate_prepared(
216 &prepared,
217 open,
218 prior,
219 current,
220 &prepared.reservation,
221 &prepared.service,
222 )?;
223 Ok(prepared)
224}
225
226pub fn verify_channel_prepared_reservation(
227 prepared: &ChannelPreparedReservationV1,
228 open: &VerifiedAdmittedChannelOpenV1,
229 prior: &VerifiedChannelStateV1,
230 current: &chio_core::economic_continuity::VerifiedEconomicStateView,
231 expected_reservation: &ChannelReservationBodyV1,
232 expected_service: &ChannelServiceBindingV1,
233) -> Result<VerifiedChannelPreparedReservationV1, ChannelError> {
234 validate_prepared(
235 prepared,
236 open,
237 prior,
238 current,
239 expected_reservation,
240 expected_service,
241 )?;
242 Ok(VerifiedChannelPreparedReservationV1 {
243 prepared: prepared.clone(),
244 open: open.clone(),
245 prior: prior.clone(),
246 current: current.clone(),
247 })
248}
249
250fn validate_prepared(
251 prepared: &ChannelPreparedReservationV1,
252 open: &VerifiedAdmittedChannelOpenV1,
253 prior: &VerifiedChannelStateV1,
254 current: &chio_core::economic_continuity::VerifiedEconomicStateView,
255 expected_reservation: &ChannelReservationBodyV1,
256 expected_service: &ChannelServiceBindingV1,
257) -> Result<(), ChannelError> {
258 let consent = open.consent();
259 let intent = consent.intent();
260 let open_artifact = consent.artifact();
261 let body = &prepared.reservation;
262 body.validate()?;
263 prepared.service.validate()?;
264 expected_reservation.validate()?;
265 expected_service.validate()?;
266 let snapshot = verify_channel_lifecycle_snapshot(
267 current,
268 &intent.body.settlement_authority_scope_id,
269 &open_artifact.body.channel_id,
270 )?;
271 let lifecycle = snapshot.lifecycle();
272 let escrow = snapshot.escrow();
273 let open_digest = open_artifact.digest()?;
274 let prior_digest = prior.digest()?;
275 let expected_sequence = next_sequence(prior.body().seq)?;
276 let expected_reservation_id = derive_channel_reservation_id(
277 &body.channel_id,
278 &open_digest,
279 &body.request_id,
280 body.next_sequence,
281 &prior_digest,
282 )?;
283 let remaining = intent
284 .body
285 .bound
286 .units
287 .checked_sub(prior.body().cumulative_owed.units)
288 .ok_or(ChannelError::ArithmeticOverflow)?;
289 let channel_expiry_unix_ms = intent
290 .body
291 .channel_expiry_unix_secs
292 .checked_mul(1_000)
293 .ok_or(ChannelError::ArithmeticOverflow)?;
294 intent
295 .body
296 .asset_binding
297 .verify_round_trip(&body.maximum_charge, &body.maximum_token_base_units)?;
298 intent.body.asset_binding.verify_round_trip(
299 &prior.body().cumulative_owed,
300 &prior.body().cumulative_token_base_units,
301 )?;
302 if prepared.schema != CHANNEL_PREPARED_RESERVATION_SCHEMA
303 || &prepared.signed_open_intent != intent
304 || &prepared.signed_open != open_artifact
305 || prepared.prior_state != RetainedChannelStateV1::from_verified(prior)
306 || body != expected_reservation
307 || &prepared.service != expected_service
308 || &prepared.lifecycle != lifecycle
309 || &prepared.escrow != escrow
310 || prepared.channel_head_digest != snapshot.channel_head_digest()
311 || prepared.escrow_head_digest != snapshot.escrow_head_digest()
312 || prepared.anchor_id != current.view().anchor_id
313 || prepared.namespace != current.view().namespace
314 || prepared.checkpoint_sequence != current.view().checkpoint_sequence
315 || prepared.checkpoint_digest != current.view().checkpoint_digest
316 || prepared.observed_at_unix_ms != current.view().observed_at
317 || snapshot.channel_head() != open.snapshot().channel_head()
318 || snapshot.escrow_head() != open.snapshot().escrow_head()
319 || body.reservation_id != expected_reservation_id
320 || body.channel_id != open_artifact.body.channel_id
321 || body.open_digest != open_digest
322 || open_artifact.body.open_intent_digest != intent.digest()?
323 || body.request_id != prepared.service.request.request_id
324 || body.service_binding_digest != prepared.service.digest()?
325 || body.next_sequence != expected_sequence
326 || body.prior_state_digest != prior_digest
327 || body.maximum_charge.currency != intent.body.currency
328 || body.maximum_charge.units > remaining
329 || parse_base_units(&body.maximum_token_base_units)?
330 > parse_base_units(&intent.body.bound_token_base_units)?
331 || prior.body().cumulative_owed.currency != intent.body.currency
332 || prior.body().asset_binding_digest != intent.body.asset_binding.digest()?
333 || lifecycle.status != ChannelLifecycleStatusV1::Open
334 || lifecycle.latest_state_digest != prior_digest
335 || lifecycle.latest_sequence != prior.body().seq
336 || lifecycle.state_version != body.channel_state_expected_version
337 || lifecycle.lifecycle_fence != body.lifecycle_fence
338 || lifecycle.live_reservation_id.is_some()
339 || lifecycle.operation_id.is_some()
340 || escrow.status != ChannelEscrowReservationStatusV1::Open
341 || escrow.open_digest != open_digest
342 || escrow.escrow_reference != intent.body.escrow_reference
343 || body.expires_at_unix_ms <= current.view().observed_at
344 || body.expires_at_unix_ms > channel_expiry_unix_ms
345 {
346 return Err(ChannelError::AuthorityVerification);
347 }
348 Ok(())
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
352#[serde(rename_all = "camelCase")]
353struct ChannelReservationTransitionProofV1 {
354 schema: String,
355 open_intent_digest: String,
356 open_digest: String,
357 prior_state_digest: String,
358 reservation_proposal_digest: String,
359 reservation_digest: String,
360 operation_id: String,
361 request: EconomicRequestBindingV1,
362 provider: EconomicEffectTargetV1,
363 source_checkpoint_digest: String,
364 source_channel_head_digest: String,
365 source_escrow_head_digest: String,
366 terminal_channel_head_digest: String,
367 terminal_escrow_head_digest: String,
368 ready_effect_digest: String,
369 ready_effect_head_digest: String,
370 issued_at: u64,
371}
372
373impl ChannelReservationTransitionProofV1 {
374 fn digest(&self) -> Result<String, ChannelError> {
375 digest(CHANNEL_RESERVATION_TRANSITION_PROOF_DOMAIN, self)
376 }
377}
378
379pub fn compose_channel_reservation_transition(
380 verified: &VerifiedChannelPreparedReservationV1,
381 proposal: &VerifiedChannelReservationProposalV1,
382 issued_at: u64,
383) -> Result<ChannelLifecycleProjectionV1, ChannelError> {
384 let prepared = &verified.prepared;
385 let body = &prepared.reservation;
386 if &proposal.artifact().body != body
387 || issued_at < proposal.accepted_at_unix_ms()
388 || issued_at < verified.current.view().observed_at
389 || issued_at >= body.expires_at_unix_ms
390 {
391 return Err(ChannelError::AuthorityVerification);
392 }
393 validate_prepared(
394 prepared,
395 &verified.open,
396 &verified.prior,
397 &verified.current,
398 body,
399 &prepared.service,
400 )?;
401 let lifecycle = &prepared.lifecycle;
402 let escrow = &prepared.escrow;
403 let lifecycle_fence = lifecycle
404 .lifecycle_fence
405 .checked_add(1)
406 .ok_or(ChannelError::ArithmeticOverflow)?;
407 let state_version = lifecycle
408 .state_version
409 .checked_add(1)
410 .ok_or(ChannelError::ArithmeticOverflow)?;
411 let escrow_version = escrow
412 .version
413 .checked_add(1)
414 .ok_or(ChannelError::ArithmeticOverflow)?;
415 let reserved_lifecycle = ChannelLifecycleViewV1 {
416 state_version,
417 lifecycle_fence,
418 live_reservation_id: Some(body.reservation_id.clone()),
419 operation_id: Some(body.operation_id.clone()),
420 ..lifecycle.clone()
421 };
422 let reserved_escrow = ChannelEscrowReservationViewV1 {
423 version: escrow_version,
424 lifecycle_fence,
425 ..escrow.clone()
426 };
427 reserved_lifecycle.validate()?;
428 reserved_escrow.validate()?;
429 let channel_key = EconomicResourceKeyV1 {
430 resource_family: CHANNEL_LIFECYCLE_RESOURCE_FAMILY.to_owned(),
431 scope_id: prepared
432 .signed_open_intent
433 .body
434 .settlement_authority_scope_id
435 .clone(),
436 resource_id: body.channel_id.clone(),
437 };
438 let escrow_key = EconomicResourceKeyV1 {
439 resource_family: CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY.to_owned(),
440 scope_id: prepared
441 .signed_open_intent
442 .body
443 .settlement_authority_scope_id
444 .clone(),
445 resource_id: body.channel_id.clone(),
446 };
447 let current_channel_head = verified
448 .current
449 .view()
450 .head(&channel_key)
451 .ok_or(ChannelError::AuthorityVerification)?;
452 let current_escrow_head = verified
453 .current
454 .view()
455 .head(&escrow_key)
456 .ok_or(ChannelError::AuthorityVerification)?;
457 let idempotency_key = derive_channel_service_dispatch_idempotency_key(
458 &body.operation_id,
459 &body.reservation_id,
460 body.next_sequence,
461 )?;
462 let next_channel_head = successor_head(
463 current_channel_head,
464 &reserved_lifecycle,
465 SuccessorHeadBinding {
466 resource_version: state_version,
467 lifecycle_fence,
468 lifecycle_state: "open",
469 operation_id: Some(body.operation_id.clone()),
470 effect_idempotency_key: Some(idempotency_key.clone()),
471 terminal_result: None,
472 },
473 issued_at,
474 )?;
475 let next_escrow_head = successor_head(
476 current_escrow_head,
477 &reserved_escrow,
478 SuccessorHeadBinding {
479 resource_version: escrow_version,
480 lifecycle_fence,
481 lifecycle_state: "open",
482 operation_id: Some(body.operation_id.clone()),
483 effect_idempotency_key: Some(idempotency_key.clone()),
484 terminal_result: None,
485 },
486 issued_at,
487 )?;
488 let reservation_digest = proposal.artifact().digest()?;
489 let mut ready_effect = EconomicEffectSlotV1 {
490 schema: CHIO_ECONOMIC_EFFECT_SLOT_SCHEMA.to_owned(),
491 slot_id: String::new(),
492 anchor_id: verified.current.view().anchor_id.clone(),
493 namespace: verified.current.view().namespace.clone(),
494 resource_key: channel_key.clone(),
495 operation_id: body.operation_id.clone(),
496 effect_kind: CHANNEL_SERVICE_DISPATCH_EFFECT_KIND.to_owned(),
497 request: prepared.service.request.clone(),
498 admission_handoff: prepared.service.admission_handoff.clone(),
499 target: prepared.service.provider.clone(),
500 action_digest: prepared.service.action_digest.clone(),
501 parameters_digest: reservation_digest.clone(),
502 resource_head_digest: head_digest(&next_channel_head)?,
503 frost: None,
504 idempotency_key: idempotency_key.clone(),
505 state: EconomicEffectStateV1::Ready,
506 terminal: None,
507 };
508 ready_effect.slot_id = ready_effect
509 .recompute_slot_id()
510 .map_err(|_| ChannelError::AuthorityVerification)?;
511 ready_effect
512 .validate()
513 .map_err(|_| ChannelError::AuthorityVerification)?;
514 let ready_effect_digest = ready_effect
515 .digest()
516 .map_err(|_| ChannelError::AuthorityVerification)?;
517 let ready_effect_head = ready_effect_head(&ready_effect, issued_at)?;
518 let request_replay = EconomicRequestReplayV1 {
519 request: ready_effect.request.clone(),
520 operation_id: body.operation_id.clone(),
521 effect_slot_ids: vec![ready_effect.slot_id.clone()],
522 };
523 request_replay
524 .validate()
525 .map_err(|_| ChannelError::AuthorityVerification)?;
526 let source_channel_head_digest = head_digest(current_channel_head)?;
527 let source_escrow_head_digest = head_digest(current_escrow_head)?;
528 let proof = ChannelReservationTransitionProofV1 {
529 schema: CHANNEL_RESERVATION_TRANSITION_PROOF_SCHEMA.to_owned(),
530 open_intent_digest: prepared.signed_open_intent.digest()?,
531 open_digest: prepared.signed_open.digest()?,
532 prior_state_digest: verified.prior.digest()?,
533 reservation_proposal_digest: body.proposal_digest()?,
534 reservation_digest,
535 operation_id: body.operation_id.clone(),
536 request: ready_effect.request.clone(),
537 provider: ready_effect.target.clone(),
538 source_checkpoint_digest: verified.current.view().checkpoint_digest.clone(),
539 source_channel_head_digest: source_channel_head_digest.clone(),
540 source_escrow_head_digest: source_escrow_head_digest.clone(),
541 terminal_channel_head_digest: head_digest(&next_channel_head)?,
542 terminal_escrow_head_digest: head_digest(&next_escrow_head)?,
543 ready_effect_digest: ready_effect_digest.clone(),
544 ready_effect_head_digest: head_digest(&ready_effect_head)?,
545 issued_at,
546 };
547 let proof_digest = proof.digest()?;
548 let mut channel_transition = transition(
549 channel_key,
550 source_channel_head_digest,
551 next_channel_head,
552 &proof_digest,
553 );
554 channel_transition.prepared_effect = Some(EconomicPreparedEffectV1 {
555 operation_id: body.operation_id.clone(),
556 action_digest: ready_effect.action_digest.clone(),
557 effect_slot_id: ready_effect.slot_id.clone(),
558 effect_slot_digest: ready_effect_digest,
559 authorization: EconomicActionAuthorizationV1::Direct,
560 });
561 let mut transitions = vec![
562 channel_transition,
563 transition(
564 escrow_key,
565 source_escrow_head_digest,
566 next_escrow_head,
567 &proof_digest,
568 ),
569 EconomicStateTransitionV1 {
570 resource_key: ready_effect.resource_head_key(),
571 expected_head_digest: None,
572 next_head: ready_effect_head,
573 transition_proof_digest: proof_digest.clone(),
574 prepared_effect: None,
575 },
576 ];
577 transitions.sort_by(|left, right| left.resource_key.cmp(&right.resource_key));
578 Ok(ChannelLifecycleProjectionV1 {
579 current: verified.current.clone(),
580 proof_digest,
581 transitions,
582 effect_slots: vec![ready_effect],
583 request_replays: vec![request_replay],
584 operation_id: body.operation_id.clone(),
585 issued_at,
586 not_after_unix_ms: Some(body.expires_at_unix_ms),
587 })
588}
589
590fn ready_effect_head(
591 effect: &EconomicEffectSlotV1,
592 issued_at: u64,
593) -> Result<EconomicResourceHeadV1, ChannelError> {
594 let state = EconomicContentV1::Inline {
595 value: serde_json::to_value(effect)
596 .map_err(|error| ChannelError::Canonicalization(error.to_string()))?,
597 };
598 let head = EconomicResourceHeadV1 {
599 schema: CHIO_ECONOMIC_RESOURCE_HEAD_SCHEMA.to_owned(),
600 anchor_id: effect.anchor_id.clone(),
601 namespace: effect.namespace.clone(),
602 resource_key: effect.resource_head_key(),
603 head_version: 1,
604 resource_version: 1,
605 lifecycle_fence: 1,
606 lifecycle_state: "ready".to_owned(),
607 state_digest: state
608 .digest()
609 .map_err(|_| ChannelError::AuthorityVerification)?,
610 state,
611 operation_id: Some(effect.operation_id.clone()),
612 effect_idempotency_key: Some(effect.idempotency_key.clone()),
613 frost: None,
614 terminal_result: None,
615 trusted_clock_high_water: issued_at,
616 predecessor_digest: None,
617 };
618 head.validate()
619 .map_err(|_| ChannelError::AuthorityVerification)?;
620 Ok(head)
621}