EconomyCredit, Insurance & Risk
Risk Comptroller Reports
Verify a signed risk authority report for a Transaction Passport, including arithmetic and trusted signatures.
Use this page when
Implementation and schema
crates/platform/chio-risk-comptroller/src/lib.rs with reserve-ledger logic in crates/platform/chio-risk-comptroller/src/ledger.rs. The wire schema is spec/schemas/chio-risk/v1/comptroller-report.schema.json. Names, constants, and defaults below match those files exactly.What the Comptroller Checks
The crate is a pure verifier: no I/O, no runtime state. It treats every report as untrusted input. Its only external inputs are the trusted-key list passed to the signature checks and the contains_ref callback passed to evidence-ref resolution. Three things are checked, in this order:
- Signature. The report carries an Ed25519 signature over its own body; the signer's public key must be in a caller-supplied trusted set.
- Internal consistency. Every nested record cross-references the others: facility state and lifecycle replay, coverage binding, reconciliation balance, premium binding, actuarial backtest limits, capital decomposition and instructions, reserve and sanction reserve ledger arithmetic, and claim appeals.
- Portfolio adequacy. For a set of reports sharing a facility, aggregate capital adequacy holds and no reserve is consumed twice across reports.
Every failure the crate returns is the single error variant TransactionPassportError::RiskComptrollerClaimFailed. Nothing else is ever constructed. A report only becomes a set of verified_claims() after signature, its cross-references, and its ledger arithmetic all check out.
Do not confuse this verifier with the Comptroller Console, which shares the comptroller name but does a different job. This page verifies one signed capital and risk authority report as evidence for a Transaction Passport. The Console reads the live receipt log over the receipt log: a spend-event stream, budget-utilization webhooks, burn-rate projections, and spend-anomaly findings. The Console observes and projects and verifies nothing for a passport; this verifier admits or rejects one authority report and enforces nothing at runtime.
Public API
Five entry points, plus two public types. Every other struct in the schema is private, so the report shape is an implementation detail behind the validators.
// Full internal-consistency check against a Transaction Passport.
pub fn validate_risk_report(
passport: &TransactionPassport,
report: &RiskComptrollerReport,
) -> Result<(), TransactionPassportError>;
// Signature check, deserialize, then validate_risk_report.
pub fn validate_signed_risk_report(
passport: &TransactionPassport,
report_value: &serde_json::Value,
trusted_authority_keys: &[PublicKey],
) -> Result<RiskComptrollerReport, TransactionPassportError>;
// Signature check only, against a raw JSON value.
pub fn validate_risk_report_signature(
report_value: &serde_json::Value,
trusted_authority_keys: &[PublicKey],
) -> Result<(), TransactionPassportError>;
// Cross-report capital adequacy and reserve consumption.
pub fn validate_risk_portfolio_reports(
reports: &[RiskComptrollerReport],
) -> Result<(), TransactionPassportError>;
// Evidence-graph membership check via caller callback.
pub fn validate_risk_evidence_refs(
report: &RiskComptrollerReport,
contains_ref: impl FnMut(&str, RiskEvidenceRefKind) -> bool,
) -> Result<(), TransactionPassportError>;On RiskComptrollerReport, only id, order_id, subject, and signature are public fields; verified_claims() returns the claim ids the comptroller asserts. Evidence refs cited inside a report are resolved through a callback so the crate stays independent of any evidence-graph representation:
pub enum RiskEvidenceRefKind {
AuthorityReceipt,
SupportingEvidence,
ReserveLedgerReceipt,
Settlement,
Jurisdiction,
}Report Envelope and Top-Level Gates
The report deserializes into RiskComptrollerReport. Every nested struct derives #[serde(deny_unknown_fields)], so an unrecognized field fails closed at deserialization, before any validator runs.
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RiskComptrollerReport {
schema: String, // chio.risk.comptroller-report.v1
pub id: String,
issued_at: String,
passport_id: String,
pub order_id: String,
pub subject: String,
verdict: String, // must be "verified"
risk_state: String, // must be "reconciled"
pub signature: String,
facility: RiskFacilityState,
facility_lifecycle: Vec<RiskFacilityTransition>,
coverage: RiskCoverageBinding,
reconciliation: RiskReconciliation,
premium: Option<RiskPremiumBinding>,
actuarial_evidence: RiskActuarialEvidence,
insurance_copy: RiskInsuranceCopy,
capital_decomposition: Option<RiskCapitalDecomposition>,
capital_instructions: Vec<RiskCapitalInstruction>,
reserve_ledger: Vec<RiskReserveLedgerEntry>,
sanction_reserve_ledger: Vec<RiskSanctionReserveLedgerEntry>,
appeals: Vec<RiskClaimAppeal>,
verified_claims: Vec<String>,
}validate_risk_report starts with a fixed set of envelope gates before descending into the sub-records:
- Every scalar id field is non-empty, and
schemaequalschio.risk.comptroller-report.v1. passport_idequals the verifying passport's id.verdictisverifiedandrisk_stateisreconciled. Arejectedreport is never admitted.verified_claimscontains the marker claimclaim.risk.comptroller_report_bound.
Although premium and capital_decomposition are Option on the Rust type, both the schema and the validators require them present; a report missing either fails with risk premium binding missing or risk capital decomposition missing.
Authority Signature
The signature travels on the report and is verified over the report body with the signature field removed. The wire form is a colon-delimited triple:
sig-ed25519:<public-key-hex>:<signature-hex>validate_risk_report_signature rejects an empty trusted-key list up front, parses the prefix, resolves the embedded public key against trusted_authority_keys, strips the signature field, and verifies the remainder via PublicKey::verify_canonical (RFC 8785 canonical JSON). An untrusted signer, a malformed prefix, or a failed verification all fail closed. See Receipts for the same canonical-JSON signing model used across Chio.
Facility State and Lifecycle Replay
The facility block declares the facility's current state, its capital and reserve amounts, and a reserve_ref. Reserve must be non-zero, must not exceed capital, and must share the capital currency. The facility state is one of twelve ordered stages:
| State | Reading |
|---|---|
evidence_cold | Replay origin; no facility yet |
underwriting_ready | Underwriting inputs assembled |
facility_granted | Facility committed |
reserve_held | Reserve carved out of capital |
capital_allocatable | Capital ready to allocate |
coverage_bound | Coverage bound against the reserve |
claim_open | A claim is open on the coverage |
claim_decided | Claim adjudicated |
payout_matched | Payout observed and reconciled |
settlement_matched | Settlement observed and reconciled |
reserve_controlled | Residual reserve under control |
closed | Facility fully reconciled and closed |
The facility_lifecycle is a set of signed transitions, each carrying an authority_receipt_ref and an evidence_ref. Validation replays a state machine from evidence_cold using this edge set as the only allowed transitions:
evidence_cold -> underwriting_ready
underwriting_ready -> facility_granted
facility_granted -> reserve_held
reserve_held -> capital_allocatable
reserve_held -> coverage_bound
capital_allocatable -> coverage_bound
coverage_bound -> claim_open
coverage_bound -> settlement_matched
claim_open -> claim_decided
claim_decided -> payout_matched
payout_matched -> settlement_matched
settlement_matched -> reserve_controlled
reserve_controlled -> closedReplay does not trust input order: it starts at evidence_cold and consumes each transition exactly once until it reaches the declared facility.state. Transitions may arrive in any order, but each from_state must be unique, every transition must be used, and the final state must match. A declared state of coverage_bound or later requires a non-empty lifecycle, so a bound coverage always carries the replay that produced it.
Coverage, Capital Adequacy, and Reconciliation
The coverage block binds an exposure to the facility reserve. It must reference the same order_id and subject as the report, the same reserve_ref and currency as the facility, a status of bound, and a facility state that reaches coverage_bound. The core solvency invariant is checked here with checked_add:
exposure_units + reserve_units <= capital_units // adequacy
exposure_units <= capital_units // exposure fits capital
reserve_units <= capital_units // reserve fits capitalThe reconciliation block then proves the money ledger balances against coverage and facility: its currency matches the coverage, its exposure_units match the coverage, its reserve_units match the facility, consumed_reserve_units never exceeds the reserve, payout_units equals settlement_units, and status is balanced. When the facility is closed, consumed reserve must equal the full reserve, so a closed facility can leave nothing unreconciled.
Premium Binding
The premium block ties the bound coverage to a quote. It must reference the same coverage id, order, subject, currency, and exposure, and the bound premium must equal the quoted premium. Status is one of two states:
bound: premium quoted and bound, not yet collected;collected_premium_unitsmust be zero.collected:collected_premium_unitsequals the bound premium, and at least one ofobserved_payment_reforsettlement_refis present as collection evidence.
For how the premium amount is priced upstream, see Underwriting Risk Taxonomy. The comptroller does not re-price; it checks that the bound and collected amounts are self-consistent.
Actuarial Evidence and Insurance Copy
The actuarial_evidence block carries a model reference, a supported exposure ceiling, a confidence level in basis points (1..=10000), and a backtest. The backtest must have status: passed, a non-inverted RFC 3339 window, a non-zero sample, loss ratios inside 0..=10000 bps, and an observed loss ratio no greater than its maximum. Coverage exposure must not exceed the supported exposure.
The insurance_copy is the authority's coverage statement. Its actuarial_evidence_ref must equal the actuarial model_ref, and its maximum_coverage_units must equal the bound coverage exposure exactly: undercovering the exposure fails with risk insurance copy undercovers exposure, and exceeding it fails with risk insurance copy exceeds bound coverage. The copy can never quietly widen or narrow the coverage the rest of the report was built on.
Capital Decomposition
The capital_decomposition breaks the facility commitment into buckets. Its source_kind is facility_commitment, its currency matches the facility, its committed_units equals the facility capital, and its held_units equals the facility reserve. The available balance is derived, not asserted:
available_units = committed_units
- (held_units + drawn_units + disbursed_units + impaired_units)The deductions are summed with checked_add (overflow is a validation error, not a panic) and the remainder with saturating_sub; a declared available_units that disagrees fails with risk capital available mismatch.
Reserve Ledger
The reserve_ledger is the list of movements against the reserve. Each entry is scoped to the facility's reserve_ref and to a claim id inside coverage.covered_claim_ids. Entry ids and receipt refs are unique within the report. The lane vocabulary:
| Lane | Terminal | Meaning |
|---|---|---|
claim_payout | Yes | Reserve paid out on a claim; binds a capital instruction |
reserve_release | Yes | Reserve released back to available capital |
reserve_slash | Yes | Reserve slashed; reversible by reverse_slash |
market_slash | Yes | Slash driven by a sanction; requires a sanction bridge |
write_off | Yes | Unrecoverable balance formalized |
hold | No | Reserve held; does not consume |
reverse_slash | No | Unwinds units from a prior reserve_slash on the same pair |
Terminal reserve consumption is single-use per (reserve_ref, claim_id) pair within a report; a second terminal entry on the same pair fails with risk reserve double consumption. A reverse_slash can only unwind units already consumed by a prior reserve_slash on the same pair, and never more than were slashed. The running totals must close: summed consumption equals reconciliation.consumed_reserve_units, and summed claim_payout units equal reconciliation.payout_units.
Settlement Counterparties
A claim_payout entry must carry both a payer_subject and a payee_subject: the payer is the coverage subject, and the payee is the coverage's beneficiary_subject when one is set, otherwise the subject itself. Any non-payout lane that carries counterparties is rejected, so counterparty topology only ever appears where funds actually move.
Capital Instructions
Every claim_payout reserve entry must bind exactly one RiskCapitalInstruction, keyed by reserve_entry_id and matching the entry's order, claim, reserve, currency, units, and settlement ref. The instruction must be pre-observed, so a report can never assert a payout that has already executed:
intended_action: transfer_funds
source_kind: facility_commitment
intended_state: pending_execution
reconciled_state: not_observed
observed_execution_ref: <absent>If there are no payout entries there must be no instructions; a dangling instruction fails with risk capital instruction unbound. The observed side of the payout is admitted separately downstream; see Claims Lifecycle for the payout instruction and receipt stages, and Reconciliation for how rail observations close instructions.
Market Slash and the Sanction Bridge
A market_slash entry requires a sanction_bridge bound to the coverage subject and capped by maximum_slash_units; the entry amount may not exceed that cap. Each market slash then maps one-to-one to an entry in the sanction_reserve_ledger, which replays the same receipt, reserve, claim, currency, units, and settlement ref plus the bridge's authority receipt, evidence, and jurisdiction refs. A market slash without a matching sanction entry, or a sanction entry without a market slash, both fail closed.
Claim Appeals
A RiskClaimAppeal carries a status of open, resolved, denied, or withdrawn, and a non-empty blocks list drawn from claim_payout, reserve_release, reserve_slash, market_slash, facility_closure, and write_off. Appeal ids are unique and every appeal must name a claim inside coverage.covered_claim_ids.
An open appeal blocks only its own claim, and only the lanes it names. A terminal reserve entry on a blocked lane for that claim fails with risk open appeal blocks reserve action, and a closed facility with an open appeal naming facility_closure fails with risk open appeal blocks facility closure. Resolved, denied, and withdrawn appeals block nothing.
Portfolio Adequacy Across Reports
A caller holding several reports against the same facility calls validate_risk_portfolio_reports to check aggregate solvency. It groups reports into two kinds of pool:
- A capital pool keyed by
(subject, capital_currency). All reports in a pool must agree oncapital_units; the sum of each report's exposure plus each distinct reserve's units may not exceed the pooled capital. - A reserve pool keyed by
reserve_ref. All reports sharing a reserve ref must agree on its facility id, currency, and units; summedconsumed_reserve_unitsmay not exceed the reserve.
The check rejects a capital adequacy breach, reserve overconsumption, a (reserve_ref, claim_id) pair consumed by a terminal lane in more than one report, a reserve ledger receipt ref reused across reports, and a reserve ref reused across different facility ids. It catches the double-spend that no single report can see on its own.
Binding to an Evidence Graph
A report references ids for authority receipts, supporting evidence, reserve-ledger receipts, settlement, and jurisdiction. validate_risk_evidence_refs walks all of them and calls the caller's contains_ref closure with the ref and its RiskEvidenceRefKind; the crate never reads the graph itself. In the proof-room verifier, that closure resolves each kind to a set of allowed node schemas:
| RiskEvidenceRefKind | Resolves to node schema |
|---|---|
AuthorityReceipt | chio.enterprise.approval-case.v1, chio.risk.guarantee-decision.v1, chio.receipt.v1 |
SupportingEvidence | chio.enterprise.data-governance-report.v1, chio.enterprise.evidence-export-bundle.v1, chio.enterprise.telemetry-projection.v1, chio.enterprise.control-evidence-map.v1, chio.commerce.provider-selection-report.v1 |
ReserveLedgerReceipt | chio.enterprise.approval-case.v1, chio.risk.guarantee-decision.v1, chio.receipt.v1 |
Settlement | chio.enterprise.evidence-export-bundle.v1, chio.web3-settlement-execution-receipt.v2, chio.web3-settlement-proof-bundle.v1, chio.receipt.v1 |
Jurisdiction | chio.risk.adjudication-jurisdiction-receipt.v1, chio.enterprise.approval-case.v1, chio.receipt.v1 |
How a Report Is Admitted
chio-control-plane re-exports the crate as chio_control_plane::risk_comptroller and is consumed by chio-commerce-order, chio-enterprise-export, chio-trust-market-context, and chio-proof-room wherever a report needs to be admitted as evidence for a Transaction Passport. The report rides in a proof bundle's evidence graph under the node role risk-comptroller-report.
The CLI path is chio proof verify with --require risk. It loads the single report node, checks its SHA-256 against the graph entry, verifies the signature against the trusted keys in CHIO_ENTERPRISE_TRUSTED_RISK_COMPTROLLER_KEYS, then runs validate_risk_report and validate_risk_evidence_refs against the passport and the graph:
$ export CHIO_ENTERPRISE_TRUSTED_RISK_COMPTROLLER_KEYS=3f0dda81e6abbcc5...
$ chio proof verify ./bundle/transaction-passport.json --require risk
verdict: verified
passport_id: passport-enterprise-valid
risk_comptroller_report_ref: risk-comptroller-enterprise-valid
order_id: order-commerce-001
subject: did:chio:buyer-enterprise
verified_claims: [claim.risk.comptroller_report_bound]The trusted-key env var is required: an unset or empty value fails before any report is read, so verification is never silently unauthenticated.
Worked Example
A clean autonomous-commerce report: a $100 facility (10,000 USD units) holds a 1,200-unit reserve, binds 5,000 units of coverage for did:chio:buyer-enterprise, and reaches settlement_matched with no claims filed. Relevant fields:
{
"schema": "chio.risk.comptroller-report.v1",
"id": "risk-comptroller-enterprise-valid",
"passport_id": "passport-enterprise-valid",
"order_id": "order-commerce-001",
"subject": "did:chio:buyer-enterprise",
"verdict": "verified",
"risk_state": "reconciled",
"facility": {
"facility_id": "facility-enterprise-valid",
"state": "settlement_matched",
"capital_currency": "USD", "capital_units": 10000,
"reserve_currency": "USD", "reserve_units": 1200,
"reserve_ref": "reserve-enterprise-valid"
},
"coverage": {
"coverage_id": "coverage-enterprise-valid",
"order_id": "order-commerce-001",
"subject": "did:chio:buyer-enterprise",
"currency": "USD", "exposure_units": 5000,
"reserve_ref": "reserve-enterprise-valid", "status": "bound"
},
"reconciliation": {
"order_id": "order-commerce-001", "currency": "USD",
"exposure_units": 5000, "reserve_units": 1200,
"consumed_reserve_units": 0, "payout_units": 0,
"settlement_units": 0, "status": "balanced"
},
"premium": {
"premium_id": "premium-coverage-enterprise-valid",
"coverage_id": "coverage-enterprise-valid", "order_id": "order-commerce-001",
"subject": "did:chio:buyer-enterprise", "currency": "USD",
"coverage_exposure_units": 5000, "quoted_premium_units": 50,
"bound_premium_units": 50, "collected_premium_units": 50, "status": "collected"
},
"capital_decomposition": {
"source_kind": "facility_commitment", "currency": "USD",
"committed_units": 10000, "held_units": 1200, "drawn_units": 0,
"disbursed_units": 0, "impaired_units": 0, "available_units": 8800
},
"verified_claims": ["claim.risk.comptroller_report_bound"],
"signature": "sig-ed25519:3f0dda81e6abbcc5...:bf31d283a9b6d1af..."
}The report meets these invariants. Adequacy: 5000 + 1200 = 6200 <= 10000. Available capital: 10000 - 1200 = 8800. Premium bound equals quoted (50) and, collected, equals bound. The insurance copy's maximum coverage equals the 5,000-unit exposure. The lifecycle replays from evidence_cold to settlement_matched, and with no claim payouts there are no capital instructions to bind. The verifier returns the single verified claim, and the passport admits it.
Next Steps
- Underwriting Risk Taxonomy : where the premium and risk class the report reconciles are decided.
- Claims Lifecycle : the seven-stage claim workflow whose payouts appear as reserve-ledger entries.
- Credit Facilities & Bonds : the facility and reserve capital the comptroller decomposes.
- Liability Market : coverage binding and the sanction/market-slash context.
- Reconciliation & Watchdog : how observed settlement closes the instructions this report leaves pre-observed.