chio-credit 0.1.2

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

pub fn validate_financial_source_disclosure(
    source_family: FinancialCredentialFamilyV1,
    subject: &str,
    source_signer_key: &PublicKey,
    disclosure: &FinancialSourceDisclosureV1,
) -> Result<ValidatedFinancialSourceDisclosureV1, FinancialCredentialProjectionError> {
    let artifacts = match disclosure {
        FinancialSourceDisclosureV1::Bundled { artifacts } => artifacts,
        FinancialSourceDisclosureV1::Resolver { .. } => {
            return Err(FinancialCredentialProjectionError::ResolverProofSubstrateUnavailable);
        }
    };
    if artifacts.is_empty() || artifacts.len() > MAX_SOURCE_ARTIFACTS {
        return Err(FinancialCredentialProjectionError::IncompleteSource);
    }
    let mut source_artifact_digests = Vec::with_capacity(artifacts.len());
    let mut seen = BTreeSet::new();
    for artifact in artifacts {
        validate_bundled_source_artifact(artifact)?;
        if !seen.insert(artifact.artifact_digest.as_str()) {
            return Err(FinancialCredentialProjectionError::IncompleteSource);
        }
        source_artifact_digests.push(artifact.artifact_digest.clone());
    }
    source_artifact_digests.sort();

    let (mut expected_members, maximum_source_evidence_class) = match source_family {
        FinancialCredentialFamilyV1::CreditScorecard => {
            let scorecard_artifact = one_bundled_artifact(
                artifacts,
                FinancialSourceArtifactRoleV1::Claim,
                CREDIT_SCORECARD_SCHEMA,
            )?;
            let exposure_artifact = one_bundled_artifact(
                artifacts,
                FinancialSourceArtifactRoleV1::Claim,
                EXPOSURE_LEDGER_SCHEMA,
            )?;
            let scorecard: SignedCreditScorecardReport =
                parse_bundled_source_artifact(scorecard_artifact)?;
            let exposure: SignedExposureLedgerReport =
                parse_bundled_source_artifact(exposure_artifact)?;
            inspect_source_signature(scorecard.verify_signature())?;
            if &scorecard.signer_key != source_signer_key
                || project_credit_scorecard_subject(&scorecard)?.id != subject
            {
                return Err(FinancialCredentialProjectionError::InvalidSourceSubject);
            }
            validate_scorecard_exposure_binding(&scorecard, &exposure)?;
            let members = parse_member_artifacts(artifacts)?;
            if artifacts.len() != members.len() + 2 {
                return Err(FinancialCredentialProjectionError::InvalidSourceSchema);
            }
            let expected = validate_exposure_report_members(
                &exposure,
                FinancialCredentialFamilyV1::CreditScorecard,
                &members,
            )?
            .1;
            (expected, ProvenanceEvidenceClass::Asserted)
        }
        FinancialCredentialFamilyV1::ExposureHistory => {
            let exposure_artifact = one_bundled_artifact(
                artifacts,
                FinancialSourceArtifactRoleV1::Claim,
                EXPOSURE_LEDGER_SCHEMA,
            )?;
            let exposure: SignedExposureLedgerReport =
                parse_bundled_source_artifact(exposure_artifact)?;
            inspect_source_signature(exposure.verify_signature())?;
            if &exposure.signer_key != source_signer_key
                || project_exposure_history_subject(&exposure)?.id != subject
            {
                return Err(FinancialCredentialProjectionError::InvalidSourceSubject);
            }
            let members = parse_member_artifacts(artifacts)?;
            if artifacts.len() != members.len() + 1 {
                return Err(FinancialCredentialProjectionError::InvalidSourceSchema);
            }
            let expected = validate_exposure_report_members(
                &exposure,
                FinancialCredentialFamilyV1::ExposureHistory,
                &members,
            )?
            .1;
            (expected, ProvenanceEvidenceClass::Asserted)
        }
        FinancialCredentialFamilyV1::PremiumHistory => {
            if artifacts
                .iter()
                .any(|artifact| artifact.role != FinancialSourceArtifactRoleV1::Member)
            {
                return Err(FinancialCredentialProjectionError::InvalidSourceSchema);
            }
            let decisions = artifacts
                .iter()
                .map(|artifact| {
                    if artifact.artifact_schema
                        != crate::underwriting::UNDERWRITING_DECISION_ARTIFACT_SCHEMA
                    {
                        return Err(FinancialCredentialProjectionError::InvalidSourceSchema);
                    }
                    parse_bundled_source_artifact::<SignedUnderwritingDecision>(artifact)
                })
                .collect::<Result<Vec<_>, _>>()?;
            let projected = project_premium_history_subject(&decisions)?;
            if projected.id != subject {
                return Err(FinancialCredentialProjectionError::InvalidSourceSubject);
            }
            let ordered = ordered_underwriting_decisions(&decisions)?;
            if ordered
                .first()
                .is_none_or(|decision| &decision.signer_key != source_signer_key)
            {
                return Err(FinancialCredentialProjectionError::InvalidSourceSignature);
            }
            let mut expected = Vec::with_capacity(ordered.len());
            for decision in ordered {
                expected.push(
                    expected_native_member(
                        source_family,
                        subject.to_string(),
                        decision.body.issued_at,
                        decision.body.decision_id.clone(),
                        crate::underwriting::UNDERWRITING_DECISION_ARTIFACT_SCHEMA,
                        decision,
                        ProvenanceEvidenceClass::Asserted,
                    )?
                    .1,
                );
            }
            (expected, ProvenanceEvidenceClass::Asserted)
        }
        FinancialCredentialFamilyV1::LossHistory => {
            if artifacts
                .iter()
                .any(|artifact| artifact.role != FinancialSourceArtifactRoleV1::Member)
            {
                return Err(FinancialCredentialProjectionError::InvalidSourceSchema);
            }
            let events = artifacts
                .iter()
                .map(|artifact| {
                    if artifact.artifact_schema != CREDIT_LOSS_LIFECYCLE_ARTIFACT_SCHEMA {
                        return Err(FinancialCredentialProjectionError::InvalidSourceSchema);
                    }
                    parse_bundled_source_artifact::<SignedCreditLossLifecycle>(artifact)
                })
                .collect::<Result<Vec<_>, _>>()?;
            let ordered = ordered_loss_events(&events)?;
            validate_loss_lifecycle_continuity(&ordered)?;
            if project_loss_history_subject(&ordered)?.id != subject
                || ordered
                    .first()
                    .is_none_or(|event| &event.signer_key != source_signer_key)
            {
                return Err(FinancialCredentialProjectionError::InvalidSourceSubject);
            }
            let mut expected = Vec::with_capacity(ordered.len());
            for event in ordered {
                expected.push(
                    expected_native_member(
                        source_family,
                        subject.to_string(),
                        event.body.issued_at,
                        event.body.event_id.clone(),
                        CREDIT_LOSS_LIFECYCLE_ARTIFACT_SCHEMA,
                        event,
                        ProvenanceEvidenceClass::Asserted,
                    )?
                    .1,
                );
            }
            (expected, ProvenanceEvidenceClass::Observed)
        }
        FinancialCredentialFamilyV1::SettlementReliability => {
            return Err(FinancialCredentialProjectionError::ReliabilityProofSubstrateUnavailable);
        }
    };
    expected_members.sort_by(|left, right| left.query_key.cmp(&right.query_key));
    let mut source_evidence_class = maximum_source_evidence_class;
    for member in &expected_members {
        if evidence_class_rank(member.evidence_class) < evidence_class_rank(source_evidence_class) {
            source_evidence_class = member.evidence_class;
        }
    }
    Ok(ValidatedFinancialSourceDisclosureV1 {
        source_artifact_digests,
        expected_members,
        source_evidence_class,
    })
}

fn validate_bundled_source_artifact(
    artifact: &FinancialSourceBundleArtifactV1,
) -> Result<(), FinancialCredentialProjectionError> {
    if !valid_identifier(&artifact.artifact_schema)
        || !valid_digest(&artifact.artifact_digest)
        || artifact.canonical_artifact.len() > MAX_SOURCE_ARTIFACT_BYTES
        || domain_digest(
            SOURCE_ARTIFACT_DIGEST_DOMAIN,
            artifact.canonical_artifact.as_bytes(),
        ) != artifact.artifact_digest
    {
        return Err(FinancialCredentialProjectionError::InvalidSourceSchema);
    }
    Ok(())
}

fn parse_bundled_source_artifact<T: DeserializeOwned + Serialize>(
    artifact: &FinancialSourceBundleArtifactV1,
) -> Result<T, FinancialCredentialProjectionError> {
    parse_canonical_source_artifact(&artifact.canonical_artifact)
}

fn one_bundled_artifact<'a>(
    artifacts: &'a [FinancialSourceBundleArtifactV1],
    role: FinancialSourceArtifactRoleV1,
    schema: &str,
) -> Result<&'a FinancialSourceBundleArtifactV1, FinancialCredentialProjectionError> {
    let mut matches = artifacts
        .iter()
        .filter(|artifact| artifact.role == role && artifact.artifact_schema == schema);
    let artifact = matches
        .next()
        .ok_or(FinancialCredentialProjectionError::IncompleteSource)?;
    if matches.next().is_some() {
        return Err(FinancialCredentialProjectionError::IncompleteSource);
    }
    Ok(artifact)
}

fn parse_member_artifacts(
    artifacts: &[FinancialSourceBundleArtifactV1],
) -> Result<Vec<SignedFinancialSourceMemberV1>, FinancialCredentialProjectionError> {
    artifacts
        .iter()
        .filter(|artifact| artifact.role == FinancialSourceArtifactRoleV1::Member)
        .map(|artifact| {
            if artifact.artifact_schema != FINANCIAL_SOURCE_MEMBER_SCHEMA_V1 {
                return Err(FinancialCredentialProjectionError::InvalidSourceSchema);
            }
            parse_bundled_source_artifact(artifact)
        })
        .collect()
}