Skip to main content

chio_settle/channel/projection/
prepared.rs

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}