chio-credit 0.1.2

Chio credit, capital, and bonded execution contracts
Documentation
use super::*;

pub const CLEARING_ROUND_SATISFACTION_SCHEMA: &str = "chio.clearing.round-satisfaction.v1";

const INTENT_PROGRESS_ROOT_DOMAIN: &[u8] = b"chio.clearing.intent-progress-root.v1\0";

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ClearingRoundSatisfactionBodyV1 {
    pub schema: String,
    pub round_id: String,
    pub round_core_digest: String,
    pub output_manifest_digest: String,
    pub finalization_digest: String,
    pub settlement_intent_root: String,
    pub settlement_intent_count: u64,
    pub intent_progress_root: String,
    pub reservation_root: String,
    pub reservation_count: u64,
    pub reservation_head_root: String,
    pub source_lifecycle_head_digest: String,
    pub source_lifecycle_version: u64,
    pub source_lifecycle_fence: u64,
    pub next_lifecycle_version: u64,
    pub next_lifecycle_fence: u64,
    pub authority_digest: String,
    pub disposition_authority_id: String,
    pub disposition_authority_key_epoch: u64,
    pub satisfied_at_unix_ms: u64,
}

impl ClearingRoundSatisfactionBodyV1 {
    pub fn validate(&self) -> Result<(), ClearingError> {
        if self.schema != CLEARING_ROUND_SATISFACTION_SCHEMA {
            return Err(ClearingError::InvalidField("round_satisfaction_schema"));
        }
        validate_text("satisfaction_round_id", &self.round_id)?;
        for (field, value) in [
            ("satisfaction_round_core_digest", &self.round_core_digest),
            (
                "satisfaction_output_manifest_digest",
                &self.output_manifest_digest,
            ),
            (
                "satisfaction_finalization_digest",
                &self.finalization_digest,
            ),
            (
                "satisfaction_settlement_intent_root",
                &self.settlement_intent_root,
            ),
            (
                "satisfaction_intent_progress_root",
                &self.intent_progress_root,
            ),
            ("satisfaction_reservation_root", &self.reservation_root),
            (
                "satisfaction_reservation_head_root",
                &self.reservation_head_root,
            ),
            (
                "satisfaction_source_lifecycle_head_digest",
                &self.source_lifecycle_head_digest,
            ),
            ("satisfaction_authority_digest", &self.authority_digest),
        ] {
            validate_digest(field, value)?;
        }
        validate_positive(
            "satisfaction_settlement_intent_count",
            self.settlement_intent_count,
        )?;
        validate_positive("satisfaction_reservation_count", self.reservation_count)?;
        if usize::try_from(self.settlement_intent_count)
            .map_err(|_| ClearingError::ArithmeticOverflow)?
            > MAX_CLEARING_SETTLEMENT_INTENTS
            || usize::try_from(self.reservation_count)
                .map_err(|_| ClearingError::ArithmeticOverflow)?
                > MAX_CLEARING_INPUTS
        {
            return Err(ClearingError::InvalidField("round_satisfaction_count"));
        }
        validate_positive(
            "satisfaction_source_lifecycle_version",
            self.source_lifecycle_version,
        )?;
        validate_positive(
            "satisfaction_source_lifecycle_fence",
            self.source_lifecycle_fence,
        )?;
        validate_positive(
            "satisfaction_next_lifecycle_version",
            self.next_lifecycle_version,
        )?;
        validate_positive(
            "satisfaction_next_lifecycle_fence",
            self.next_lifecycle_fence,
        )?;
        validate_text(
            "satisfaction_disposition_authority_id",
            &self.disposition_authority_id,
        )?;
        validate_positive(
            "satisfaction_disposition_authority_key_epoch",
            self.disposition_authority_key_epoch,
        )?;
        validate_positive("satisfied_at_unix_ms", self.satisfied_at_unix_ms)?;
        let next = increment(self.source_lifecycle_version)?;
        if self.source_lifecycle_version != self.source_lifecycle_fence
            || self.next_lifecycle_version != next
            || self.next_lifecycle_fence != next
        {
            return Err(ClearingError::InvalidField("round_satisfaction_fence"));
        }
        Ok(())
    }
}

pub type SignedClearingRoundSatisfactionV1 =
    SignedClearingEnvelopeV1<ClearingRoundSatisfactionBodyV1>;

pub fn prepare_clearing_round_satisfaction(
    current_round_head: &EconomicResourceHeadV1,
    reservations: &[AnchoredClearingObligationV1],
    request: &ClearingRoundRequestV1,
    signed_output: &SignedClearingRoundOutputV1,
    trust: &ClearingAuthorityTrustV1,
    authority_digest: String,
    satisfied_at_unix_ms: u64,
) -> Result<ClearingRoundSatisfactionBodyV1, ClearingError> {
    trust.validate()?;
    current_round_head
        .validate()
        .map_err(|_| ClearingError::InvalidField("current_round_head"))?;
    let record: ClearingRoundLifecycleRecordV1 = decode_inline(current_round_head)?;
    record.validate()?;
    validate_round_head(current_round_head, &record)?;
    if !matches!(
        record.state,
        ClearingRoundLifecycleStateV1::Dispatching
            | ClearingRoundLifecycleStateV1::Reconciling
            | ClearingRoundLifecycleStateV1::Incident
    ) || satisfied_at_unix_ms < current_round_head.trusted_clock_high_water
        || satisfied_at_unix_ms > trust.trusted_time_unix_ms
        || reservations
            .iter()
            .any(|reservation| reservation.head.trusted_clock_high_water > satisfied_at_unix_ms)
    {
        return Err(ClearingError::IllegalLifecycleTransition);
    }
    let mut output_trust = trust.clone();
    output_trust.trusted_time_unix_ms = request.generated_at_unix_ms;
    let output = verify_signed_netting_round(request, &output_trust, signed_output)?;
    validate_completed_intents(&record, &output)?;
    let output_manifest_digest = output.output_manifest.digest()?;
    let finalization_digest = record
        .finalization_digest
        .clone()
        .ok_or(ClearingError::IllegalLifecycleTransition)?;
    if record.round_id != output.core.round_id
        || record.round_core_digest != output.core.digest()?
        || record.output_manifest_digest.as_deref() != Some(output_manifest_digest.as_str())
        || record.reservation_root != output.core.reservation_root
        || record.reservation_count != output.core.input_count
    {
        return Err(ClearingError::AuthorityVerification);
    }
    let mut reservation_bindings = reservations
        .iter()
        .map(|reservation| reservation_binding(&record, reservation))
        .collect::<Result<Vec<_>, _>>()?;
    reservation_bindings.sort_by(|left, right| left.resource_key.cmp(&right.resource_key));
    let body = ClearingRoundSatisfactionBodyV1 {
        schema: CLEARING_ROUND_SATISFACTION_SCHEMA.to_owned(),
        round_id: record.round_id.clone(),
        round_core_digest: record.round_core_digest.clone(),
        output_manifest_digest,
        finalization_digest,
        settlement_intent_root: output.output_manifest.settlement_intent_root.clone(),
        settlement_intent_count: output.output_manifest.settlement_intent_count,
        intent_progress_root: intent_progress_root(&record.intent_progress)?,
        reservation_root: record.reservation_root.clone(),
        reservation_count: record.reservation_count,
        reservation_head_root: reservation_head_root(&reservation_bindings)?,
        source_lifecycle_head_digest: current_round_head
            .digest()
            .map_err(|_| ClearingError::InvalidField("current_round_head"))?,
        source_lifecycle_version: record.row_version,
        source_lifecycle_fence: record.fence,
        next_lifecycle_version: increment(record.row_version)?,
        next_lifecycle_fence: increment(record.fence)?,
        authority_digest,
        disposition_authority_id: trust.obligation_authority_id.clone(),
        disposition_authority_key_epoch: trust.obligation_key_epoch,
        satisfied_at_unix_ms,
    };
    body.validate()?;
    Ok(body)
}

pub fn compose_clearing_satisfaction_transition(
    current_round_head: &EconomicResourceHeadV1,
    reservations: &[AnchoredClearingObligationV1],
    request: &ClearingRoundRequestV1,
    signed_output: &SignedClearingRoundOutputV1,
    signed_satisfaction: &SignedClearingRoundSatisfactionV1,
    trust: &ClearingAuthorityTrustV1,
) -> Result<ClearingLifecycleProjectionV1, ClearingError> {
    let body = &signed_satisfaction.body;
    let expected = prepare_clearing_round_satisfaction(
        current_round_head,
        reservations,
        request,
        signed_output,
        trust,
        body.authority_digest.clone(),
        body.satisfied_at_unix_ms,
    )?;
    if expected != *body
        || signed_satisfaction.signer_key != trust.obligation_authority_key
        || !signed_satisfaction.verify_signature()?
    {
        return Err(ClearingError::AuthorityVerification);
    }
    compose_lifecycle_transition(
        current_round_head,
        reservations,
        ClearingRoundTransitionV1::Satisfy {
            satisfaction_digest: signed_satisfaction.digest()?,
            authority_digest: body.authority_digest.clone(),
        },
        body.satisfied_at_unix_ms,
    )
}

pub(super) fn validate_completed_intents(
    record: &ClearingRoundLifecycleRecordV1,
    output: &ClearingRoundOutputV1,
) -> Result<(), ClearingError> {
    if output.intents.is_empty()
        || output.intents.len() != record.intent_progress.len()
        || u64::try_from(record.intent_progress.len())
            .map_err(|_| ClearingError::ArithmeticOverflow)?
            != output.output_manifest.settlement_intent_count
    {
        return Err(ClearingError::IncompleteLifecycleProjection);
    }
    for intent in &output.intents {
        let index = record
            .intent_progress
            .binary_search_by(|progress| progress.intent_id.cmp(&intent.intent_id))
            .map_err(|_| ClearingError::IncompleteLifecycleProjection)?;
        let progress = &record.intent_progress[index];
        if progress.intent_digest != intent.digest()?
            || progress.completed_effect_slot_id.is_none()
            || progress.active_effect_slot_id.is_some()
            || progress.unknown_effect_slot_id.is_some()
        {
            return Err(ClearingError::IncompleteLifecycleProjection);
        }
    }
    Ok(())
}

pub(super) fn intent_progress_root(
    progress: &[ClearingIntentProgressV1],
) -> Result<String, ClearingError> {
    if progress.is_empty() {
        return Err(ClearingError::IncompleteLifecycleProjection);
    }
    domain_digest(INTENT_PROGRESS_ROOT_DOMAIN, &progress)
}