Risk Comptroller Reports
Check the signed risk report behind a Transaction Passport's financial claim: its arithmetic, lifecycle, authority, and cited evidence.
On this page
A Transaction Passport carries a financial claim, such as coverage bound against a reserve, only when the comptroller verifier accepts the signed risk report behind it. The verifier recomputes the report's totals, replays its facility lifecycle, and resolves every record it cites, so a trusted signature vouches for authorship and the arithmetic has to hold on its own.
The report is chio.risk.comptroller-report.v1, signed by a risk authority whose key the verifier trusts. The running example is your cover for an order an outside buyer's agent placed: larkfield-risk, your risk authority, binds USD 50.00 of coverage to a USD 100.00 facility that holds a USD 12.00 reserve. The report carries amounts in minor units, so the facility's capital is 10,000 units.
Coverage and reserve fit inside capital
exposure_units + reserve_units <= capital_units: 5,000 + 1,200 = 6,200 against 10,000. Exposure and reserve each fit capital on their own as well, and the sum uses checked_add, so an overflow is a validation error.
The verifier recomputes every total the authority signed
The example report binds 5,000 units of coverage for the buyer (did:chio:z6MkbY4…e1R) on order-77 to a facility with 10,000 units of capital and a 1,200-unit reserve. It reaches settlement_matched with no claims filed, and three totals hold:
- Capital adequacy.
5000 + 1200 = 6200 <= 10000: coverage plus reserve fits the facility's capital. - Available capital.
10000 - 1200 = 8800, with nothing drawn, disbursed, or impaired. - Premium binding. 50 units quoted, 50 bound, and 50 collected, and 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 report's single verified claim, claim.risk.comptroller_report_bound, and the passport admits it.
Read the example report
{
"schema": "chio.risk.comptroller-report.v1",
"id": "risk-comptroller-basalt-order-77",
"passport_id": "passport-basalt-order-77",
"order_id": "order-77",
"subject": "did:chio:z6MkbY4…e1R",
"verdict": "verified",
"risk_state": "reconciled",
"facility": {
"facility_id": "facility-basalt",
"state": "settlement_matched",
"capital_currency": "USD", "capital_units": 10000,
"reserve_currency": "USD", "reserve_units": 1200,
"reserve_ref": "reserve-basalt"
},
"coverage": {
"coverage_id": "coverage-basalt",
"order_id": "order-77",
"subject": "did:chio:z6MkbY4…e1R",
"currency": "USD", "exposure_units": 5000,
"reserve_ref": "reserve-basalt", "status": "bound"
},
"reconciliation": {
"order_id": "order-77", "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-basalt",
"coverage_id": "coverage-basalt", "order_id": "order-77",
"subject": "did:chio:z6MkbY4…e1R", "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..."
}Keep the capital and the reserve but claim USD 95.00 of coverage, and the commitment reaches 10,700 units, USD 7.00 past the facility's capital. The capital-adequacy check rejects that report even when a trusted authority signed it.
Three checks run in order over untrusted input
The verifier is a pure crate with no I/O and no runtime state. It treats every report as untrusted input, and its only outside inputs are the list of trusted authority keys and a contains_ref callback that resolves cited evidence.
- Signature. An Ed25519 signature over the report body, by a key in the caller's trusted set.
- Internal consistency. Every nested record agrees with 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. Across reports that share a facility, aggregate capital adequacy holds and no reserve is consumed twice.
Every failure is one error, TransactionPassportError::RiskComptrollerClaimFailed, with no field-level diagnostic. To investigate, keep the rejected input, recompute the invariants in the reference below, compare its identifiers with the passport and its supporting records, and tell a signature failure apart from an arithmetic or lifecycle one. Correct the authoritative inputs and obtain a newly signed report: editing a signed JSON field invalidates the signature.
This report is inbound financial evidence. Operator Reports covers chio.comptroller.surface-report.v1, the read model a node composes over its own receipt store at request time. Neither report enforces anything at runtime; the caller decides how an accepted or rejected result affects its application.
A facility reaches its declared state only by replaying signed transitions
facility_lifecycle is a set of signed transitions, each carrying an authority_receipt_ref and an evidence_ref. Validation replays them from evidence_cold and accepts only these edges:
fn is_allowed_risk_facility_transition(from_state: &str, to_state: &str) -> bool {
matches!(
(from_state, to_state),
("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", "closed")
)
}Replay is independent of input order. Each from_state must be unique, every transition must be used, and the walk must end at the declared facility.state. A declared state of coverage_bound or later requires a non-empty lifecycle, so bound coverage always carries the replay that produced it.
Read more in Credit Facilities, which issues the facility and reserve this lifecycle tracks.
Reports that share a facility are checked together
One report can fit its facility while a second report against the same capital overdraws it. validate_risk_portfolio_reports groups a caller's reports into two kinds of pool and checks each:
- A capital pool keyed by
(subject, capital_currency). Every report in the pool agrees oncapital_units, and the sum of each report's exposure plus each distinct reserve's units stays within the pooled capital. - A reserve pool keyed by
reserve_ref. Every report sharing the ref agrees on its facility id, currency, and units, and the summedconsumed_reserve_unitsstays within the reserve.
With the example facility, a report binding 5,000 units and a second binding 4,000 units each fit alone, at 6,200 and 5,200. Pooled, with their shared reserve counted once, they need 10,200 units of a 10,000-unit facility, and the set fails.
The check also rejects 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 shows.
A proof bundle admits the report through the CLI
A report rides in a proof bundle's evidence graph under the node role risk-comptroller-report. chio proof verify --require risk loads that node, checks its SHA-256 against the graph entry, verifies its signature against the keys pinned to the enterprise.risk-comptroller role, and runs validate_risk_report and validate_risk_evidence_refs against the passport and the graph.
chio trust pin enterprise.risk-comptroller ./risk-comptroller.pub --name larkfield-risk
chio proof verify ./basalt-order-77 --require riskpinned larkfield-risk enterprise.risk-comptroller ed25519:4f1c…a2e9
file ~/.chio/trust.toml
VERIFIED passport-basalt-order-77 1 claim
claim.risk.comptroller_report_bound
required risk: covered
trust ~/.chio/trust.tomlYou should see VERIFIED for passport passport-basalt-order-77 with its one risk claim; --json prints the full chio.transaction.verifier-report.v1 document. The pin is required: with no key pinned to enterprise.risk-comptroller, the command stops with CHIO-TRUST-ROLE-UNPINNED before any report is read, so verification never runs unauthenticated. Each other signer in a bundle has its own role, such as proof-room.bundle-signer; chio trust roles lists them.
Report fields, gates, and ledger rules
Search for reserve_ref, market_slash, or risk capital available mismatch.
The verifier lives in 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 here match those files.
Public API
Five entry points and two public types. Every other struct in the schema is private, so the report's shape stays behind the validators. The 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 through a caller callback:
pub fn validate_risk_evidence_refs(
report: &RiskComptrollerReport,
mut 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. Cited evidence refs resolve 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. The struct and every nested struct derive #[serde(deny_unknown_fields)], so an unrecognized field fails at deserialization, before any validator runs. The snippet starts below the report struct's two attribute lines, #[derive(Debug, Deserialize)] and #[serde(deny_unknown_fields)], at crates/platform/chio-risk-comptroller/src/lib.rs:15-16.
pub struct RiskComptrollerReport {
schema: String,
pub id: String,
issued_at: String,
passport_id: String,
pub order_id: String,
pub subject: String,
verdict: String,
risk_state: String,
#[serde(default)]
pub signature: String,
facility: RiskFacilityState,
#[serde(default)]
facility_lifecycle: Vec<RiskFacilityTransition>,
coverage: RiskCoverageBinding,
reconciliation: RiskReconciliation,
#[serde(default)]
premium: Option<RiskPremiumBinding>,
actuarial_evidence: RiskActuarialEvidence,
insurance_copy: RiskInsuranceCopy,
#[serde(default)]
capital_decomposition: Option<RiskCapitalDecomposition>,
#[serde(default)]
capital_instructions: Vec<RiskCapitalInstruction>,
#[serde(default)]
reserve_ledger: Vec<RiskReserveLedgerEntry>,
#[serde(default)]
sanction_reserve_ledger: Vec<RiskSanctionReserveLedgerEntry>,
#[serde(default)]
appeals: Vec<RiskClaimAppeal>,
verified_claims: Vec<String>,
}validate_risk_report starts with fixed envelope gates before it descends 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.
premium and capital_decomposition are Option on the Rust type, and both the schema and the validators require them. A report missing either fails with risk premium binding missing or risk capital decomposition missing.
Authority signature
The signature travels on the report and covers 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 with PublicKey::verify_canonical (RFC 8785 canonical JSON). An untrusted signer, a malformed prefix, and a failed verification all fail closed. Receipts use the same canonical-JSON signing model.
Facility states
The facility block declares the facility's current state, its capital and reserve amounts, and a reserve_ref. The reserve is non-zero, at most the capital, and in the capital's currency. The state is one of twelve, in replay order:
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.
Coverage, capital adequacy, and reconciliation
The coverage block binds an exposure to the facility reserve. It references the report's order_id and subject, the facility's reserve_ref and currency, a status of bound, and a facility state that reaches coverage_bound. The 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 balances the money ledger against coverage and facility: its currency matches the coverage, its exposure_units match the coverage, its reserve_units match the facility, consumed_reserve_units stays within the reserve, payout_units equals settlement_units, and status is balanced. A closed facility must have consumed its full reserve, so it leaves nothing unreconciled.
Premium binding
The premium block ties the bound coverage to a quote. It references the same coverage id, order, subject, currency, and exposure, and the bound premium equals the quoted premium. Its status is one of two:
bound: quoted and bound, withcollected_premium_unitsat zero.collected:collected_premium_unitsequals the bound premium, andobserved_payment_reforsettlement_refis present as collection evidence.
The comptroller checks that the bound and collected amounts agree and leaves pricing to Underwriting, where the premium is set.
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 has status: passed, a non-inverted RFC 3339 window, a non-zero sample, loss ratios inside 0..=10000 bps, and an observed loss ratio at most its maximum. Coverage exposure stays within the supported exposure.
The insurance_copy is the authority's coverage statement. Its actuarial_evidence_ref equals the actuarial model_ref, and its maximum_coverage_units equals the bound coverage exposure exactly. Undercovering fails with risk insurance copy undercovers exposure and overcovering with risk insurance copy exceeds bound coverage, so the copy can never widen or narrow the coverage the rest of the report rests on.
Capital decomposition
The capital_decomposition breaks the facility commitment into buckets. Its source_kind is facility_commitment, its currency matches the facility, committed_units equals the facility capital, and held_units equals the facility reserve. The available balance is derived:
available_units = committed_units
- (held_units + drawn_units + disbursed_units + impaired_units)The deductions are summed with checked_add, so an overflow is a validation error and never a panic, and the remainder is taken with saturating_sub. A declared available_units that disagrees fails with risk capital available mismatch.
Reserve ledger
The reserve_ledger lists movements against the reserve. Each entry is scoped to the facility's reserve_ref and to a claim id inside coverage.covered_claim_ids, and entry ids and receipt refs are unique within the report. The lanes:
| 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 without consuming it |
reverse_slash | No | Unwinds units from a prior reserve_slash on the same pair |
Terminal 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 unwinds only units a prior reserve_slash consumed on the same pair, up to the amount slashed. The running totals close: summed consumption equals reconciliation.consumed_reserve_units, and summed claim_payout units equal reconciliation.payout_units.
Settlement counterparties
A claim_payout entry carries both a payer_subject and a payee_subject. The payer is the coverage subject; the payee is the coverage's beneficiary_subject when one is set, otherwise the subject itself. A non-payout lane that carries counterparties is rejected, so counterparties appear only where funds move.
Capital instructions
Every claim_payout entry binds exactly one RiskCapitalInstruction, keyed by reserve_entry_id and matching the entry's order, claim, reserve, currency, units, and settlement ref. The instruction is pre-observed, so a report never asserts 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>A report with no payout entries carries no instructions; a dangling instruction fails with risk capital instruction unbound. The observed side of the payout is admitted downstream: Claims Lifecycle covers the payout instruction and receipt stages, and Reconciliation 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, and the entry amount stays within that cap. Each market slash maps one-to-one to an entry in the sanction_reserve_ledger, which repeats 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, fails closed. Liability Market covers the sanction context.
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 names 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.
Evidence references
A report cites ids for authority receipts, supporting evidence, reserve-ledger receipts, settlement, and jurisdiction. validate_risk_evidence_refs walks them all and calls the caller's contains_ref closure with each ref and its RiskEvidenceRefKind; the crate reads no graph itself. In the proof-room verifier, the closure resolves each kind to a set of allowed node schemas:
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.
Where the verifier runs
chio-control-plane re-exports the crate as chio_control_plane::risk_comptroller. chio-commerce-order, chio-enterprise-export, chio-trust-market-context, and chio-proof-room consume it wherever a report is admitted as evidence for a Transaction Passport. The CLI path is the proof dispatcher, which writes the verifier report to stdout at crates/products/chio-cli/src/cli/dispatch/proof.rs:560.