Chio/Docs

EconomyCost & Settlementnew

Clearinghouse

The clearinghouse nets a complete, reserved set of single-currency obligations into a smaller set of signed settlement instructions.

Use this page when

You need the multilateral netting algorithm, the ten-state clearing round lifecycle, or the chio.clearing.* record schemas, and you want to see how a cycle of debts collapses into a smaller set of settlement intents. Don't use this if you want how one intent actually moves capital (see Settlement Rails) or how a dispatched settlement is closed out against observed cost (see Reconciliation & Watchdog).

Multilateral Netting

Given a complete single-currency set of debts among many participants, the clearinghouse cancels offsetting flows, computes each participant's net balance, and emits the smaller set of settlement intents. A cycle in which A owes B owes C owes A collapses to its residual transfers; an acyclic chain A → B → C reduces to a single A → C transfer whenever the balances permit. The netting round emits a signed netting statement and the settlement intents it reduces to; the rail's reconciliation evidence follows later and separately.

The netting function lives in the pure chio_credit::clearing module; it adds no crate, because chio-credit already owns the canonical obligation contract it reads. The entry point is compute_netting_round, which accepts a round_id, an epoch, an algorithm_version, exactly one currency, a trusted participant-snapshot body and digest, a complete canonical atom manifest, and reservation proofs for every atom.

It produces settlement intents that a rail still has to execute. The clearinghouse does not hold funds or establish settlement finality.


The Input: Completeness-Proven Obligations

The clearinghouse never ingests exposure-ledger rows or IOU envelopes directly, because the two can describe the same receipt and reading both double-counts one debt. Its only input is the deduplicated canonical chio_credit::obligation::ObligationAtom: one immutable atom per receipt-backed debt, binding the authoritative debtor, the payee-bound creditor, the amount, and the currency. Identity is never inferred from IouEnvelope.issuer_key (a kernel signer, not a creditor) or from tool_server. The obligation and credit-note model those atoms come from is covered in Chio Paper.

Reserving an atom for a round flips its chio_credit::obligation::ObligationDisposition from per_call to clearing_reserved { round_id } transactionally. That flip is exclusive: per-call, assignment, and channel settlement paths all skip any clearing_reserved atom, so a single debt can never be settled through two paths at once. An atom already in assigned or channelized is ineligible, and duplicate obligation_id values, even with identical bytes, reject the whole manifest.

A round runs against exactly one currency. Every debtor and creditor identity is resolved through a signed, versioned participant snapshot that maps each identity or key alias to one canonical participant id and one settlement destination, and that must cover every atom and every output destination. There is no fallback mapping: unknown aliases, expired snapshots, conflicting mappings, and a proposer self-signing an otherwise untrusted mapping all reject the round.

Completeness is proven, not assumed. A bounded exposure report at its item limit is not proof that the epoch input is complete, so the source must supply a closed epoch range, start and end checkpoints, and a completeness proof. The input manifest commits to the sorted atom list and its count, and the round core binds that root and count so no atom can be added or dropped after signing. Any of the following rejects the round outright:

  • a has_more condition or an unconsumed cursor;
  • a count mismatch against the committed manifest;
  • a missing position in the epoch range;
  • a capped or paginated report standing in for the full set.

The v1 Netting Algorithm

The algorithm is deterministic: all sorting compares canonical participant-id bytes, and shuffled inputs produce byte-identical outputs. All addition, subtraction, and signed-balance conversion is checked; overflow rejects the round, every emitted amount must convert exactly to MonetaryAmount.units: u64, and there is no saturating result or arithmetic_saturated success state.

  1. Verify. Check the participant snapshot, atom digests, source receipt signatures, completeness proof, reservation proofs, exact currency, unique ids, and canonical order.
  2. Aggregate. Sum directed amounts by ordered (debtor, creditor) pair with checked u128 sums.
  3. Bilateral cancellation. For each unordered pair, subtract the smaller direction from the larger and retain at most one residual direction.
  4. Balance. Compute each participant balance from the residual graph as credits received minus debts owed. Zero balances drop; negatives partition into debtors, positives into creditors.
  5. Conserve. Assert that the absolute total debtor balance equals the total creditor balance. A conservation failure denies.
  6. Match. Sort debtors and creditors canonically, match the first debtor to the first creditor for the minimum remaining amount, advance each exhausted side, and repeat until both lists are empty.
  7. Emit. Produce one immutable settlement intent per non-zero match, one participant statement per participant, and a deterministic transformation witness that lets a verifier replay how every input atom contributed to cancellation, balances, and intents.

This greedy match conserves balances and emits at most debtors + creditors - 1 intents. It is not claimed to minimize rail fees under every fee model; it is claimed to be deterministic and balance-conserving.

Gross Debts to Netted Intents

rendering…
Four gross obligations among three participants reduce to two residual settlement intents; the A/B pair cancels bilaterally and C nets to the sole creditor.

Round records

Every record uses RFC 8785 canonical JSON with deny_unknown_fields, a versioned schema id, and a signature over an immutable body. The round core is hashed before any output exists, and its round_core_digest is the only round digest that statements and intents bind, which keeps the digest graph acyclic:

SchemaBindsRole
chio.clearing.netting-round-core.v1Round, epoch, governance scope id, currency, algorithm, participant-snapshot digest, input-manifest root and count, reservation-proof root, dispute windowThe root every leaf points back to
chio.clearing.participant-statement.v1Gross debit, gross credit, bilateral adjustment, final net balance, contributing atom digests, round_core_digestOne net position per participant
chio.clearing.settlement-intent.v1intent_id, round_core_digest, debtor and creditor participant ids, authoritative destination, amount, currency, contributing reservation root, dispatch idempotency keyOne residual transfer; carries no mutable reconciliation field
chio.clearing.output-manifest.v1round_core_digest, statement / intent / transformation roots and countsThe complete, verifiable round output
chio.clearing.participant-acceptance.v1Output-manifest digest, participant statement digest, participant id, acceptance signatureEach participant accepts the setoff
chio.clearing.round-finalization.v1round_core_digest, output-manifest digest, acceptance root and count, external lifecycle head digest, version, and fenceQuorum-gated seal; excludes the FROST proof from its own digest

A statement or intent alone is not a complete round proof: verification requires its inclusion in the signed chio.clearing.output-manifest.v1 and a valid finalization that includes every participant acceptance. Four more variants ride the same rules and are always emitted as separate records. The two reconciliation variants each stand apart from the intent they describe: chio.clearing.settlement-reconciliation.v1 (the rail outcome for one intent) and chio.clearing.zero-intent-reconciliation.v1 (outcome netted_without_rail for a finalized round that produced no intents). The remaining two, chio.clearing.round-abort.v1 and chio.clearing.round-dispute.v1, describe the round itself, not an intent.

The shapes below are an illustrative round output for the netting example above, not a signed wire schema; the field names track the record bodies:

netting-output.json
{
  "participant_statement": {
    "schema": "chio.clearing.participant-statement.v1",
    "round_core_digest": "b3:9f2c...",
    "participant_id": "cp:acme",
    "currency": "USD",
    "gross_debit_units": 50,
    "gross_credit_units": 30,
    "bilateral_adjustment_units": 20,
    "net_balance_units": -20,
    "contributing_atom_digests": ["b3:1a4c...", "b3:77e1..."]
  },
  "settlement_intent": {
    "schema": "chio.clearing.settlement-intent.v1",
    "intent_id": "int:round-4471:0",
    "round_core_digest": "b3:9f2c...",
    "debtor_participant_id": "cp:acme",
    "creditor_participant_id": "cp:globex",
    "destination": "did:chio:c87a...",
    "amount": { "units": 20, "currency": "USD" },
    "contributing_reservation_root": "b3:5d90...",
    "dispatch_idempotency_key": "round-4471:int-0"
  }
}

One Fenced Round

A round is a single fenced state machine. Its lifecycle contract is backend-neutral in chio_credit::clearing and implemented by chio-store-sqlite. Every transition is an authority-authenticated local stage followed by an external EconomicStateAnchor compare-and-swap over the round and its obligations; readiness requires the local head digest to equal the external head, and age is never reservation-release authority.

chio_credit::clearing
pub enum RoundLifecycleState {
    Reserved,
    Proposed,
    Finalizing,
    Finalized,
    Dispatching,
    Reconciling,
    Satisfied,
    Aborting,
    Aborted,
    Incident,
}

pub struct RoundLifecycleRecord {
    pub round_id: String,
    pub round_core_digest: String,
    pub input_manifest_digest: String,
    pub state: RoundLifecycleState,
    pub row_version: u64,
    pub fence: u64,
    pub output_manifest_digest: Option<String>,
    pub finalization_digest: Option<String>,
    pub abort_digest: Option<String>,
    pub first_dispatch_operation_id: Option<String>,
    pub continuity_head_digest: String,
}

The happy path advances Reserved → Proposed → Finalizing → Finalized → Dispatching → Reconciling → Satisfied, each hop gated on the external head at the expected row_version and fence:

  1. Reserve. Stage the signed round core plus every atom reservation and advance one external batch to Reserved. Only the anchored batch makes reservations authoritative, so recovery can always name the round that owns an atom.
  2. Propose and accept. Compute statements, intents, and transformation rows against round_core_digest, persist the signed output manifest, and open the dispute window; every affected participant then signs its statement and the manifest digest. A missing acceptance, an in-window dispute, or an incomplete quorum blocks finalization.
  3. Finalize. Advance Proposed → Finalizing, obtain the quorum authorization over the proof-free body, then consume it once to reach Finalized and bind the proof and finalization digests.
  4. Dispatch, reconcile, satisfy. Each immutable intent is dispatched through its own fenced effect slot and closed by a separate reconciliation record; effect-possible ambiguity moves the round to Incident while preserving every reservation. Only after every intent has an exact settled slot does one batch advance the round and every input obligation to Satisfied. The atoms never change.

Abort competes through the same external head: Aborting → Aborted may run only after rechecking zero dispatch, and a Finalizing round must durably cancel and burn its quorum session first. Before the first dispatch a failed round keeps its atoms unavailable to every other settlement path; after the first dispatch they stay reserved until all intents settle or a compensating reconciliation is authorized. No atom silently returns to per_call.


Quorum-Gated Finalization

Participant acceptances alone do not finalize a round. Finalization is gated on a FROST threshold signature: a detached chio.frost.authorization.v1 using domain chio.frost.clearing-round-finalize.v1, registered ladder action class clearing.round_finalize, a scope_id equal to the round core's trusted governance scope, and a resource_id equal to the round id. It binds the finalization action digest, the active roster digest, the key epoch, the authorization-slot id, and the lifecycle version and fence.

The Finalizing → Finalized transition is the single point at which that authorization is consumed, against a permanent completed authorization slot, so a round can never be finalized twice on the same proof. The group signature proves threshold group authorization, not the exact signer subset; the individual participant acceptances remain attributable evidence and are never a fallback for the quorum.

What a netting statement is not

A signed round is netting evidence, not settlement truth. compute_netting_round emits chio.clearing.settlement-intent.v1 records; a rail still has to move the money, and settlement truth lives only in the separate chio.clearing.settlement-reconciliation.v1 evidence that rail returns. Finalization is quorum-gated: with no production FROST substrate wired in, the Finalizing → Finalized transition returns UnsupportedQuorum and the round stays non-dispatchable, so on devnet no round reaches Dispatching. The clearinghouse takes no custody, claims no on-chain or bank finality, and makes no availability claim; a reserved ObligationAtom stays exclusively clearing_reserved until reconciliation evidence is applied.

See Also

  • Chio Paper for the obligation and credit-note model that produces the canonical ObligationAtom a round reserves.
  • Settlement Rails for the rail that actually executes a chio.clearing.settlement-intent.v1.
  • Reconciliation & Watchdog for how a dispatched settlement is closed and reconciled against observed cost.
  • Congregates for the multi-party operator structures that a clearing round nets across.