Chio/Docs

EconomyCost & Settlementnew

Payment Channels

Fund one bounded escrow, reserve capacity for repeated calls, and settle the cumulative signed balance once at close.

Use this page when

You are settling many small calls between the same two parties and want to fund once, meter as the stream runs, and settle a single cumulative amount instead of paying to settle every call on chain. Don't use this if you need the underlying escrow lifecycle a channel opens against (see On-Chain Settlement) or the rail-selection matrix and off-chain alternatives (see Settlement Rails). A one-off or infrequent call settles more simply per call; a channel only pays off across a high-frequency stream.

Where this fits

A channel is a bilateral overlay on top of one ChioEscrow deposit. It adds no new Solidity. On-Chain Settlement documents the escrow lifecycle; this page documents the off-chain reservation, cumulative state, and close protocol that sit above it. The channel module is chio_settle::channel: a pure record-and-validator layer that v1 adds without a new crate, because the existing settlement owner is already the dependency boundary.

Why Stream Through a Channel

Per-call on-chain settlement is fine for occasional calls. A high-frequency, low-value stream between two fixed parties pays a settlement cost per call that can dwarf the value of the call itself. A channel amortizes that: fund one bounded bilateral escrow up front, reserve capacity before each dispatch, accumulate one running cumulative signed amount across many calls, and release once at close.

The route is chosen before dispatch, never after. A post-persist observer is too late to pick a settlement path, because by then the tool has already delivered value. So a channelized receipt is committed to exactly one disposition: it becomes one immutable chio_credit::obligation::ObligationAtom with a single channelized disposition through the shared chio_credit::obligation::ObligationDisposition store. Routing is exclusive: a channelized receipt never also enters the per-call, assignment, or clearing settlement paths, and a receipt carrying a channel binding is never eligible for per-call dispatch even when the disposition projection is momentarily unavailable.

Channel records

Every channel record uses RFC 8785 canonical JSON with deny_unknown_fields, a versioned schema identifier, and a signature over an immutable body. Nine schemas span the lifecycle:

SchemaRole
chio.channel.funding-evidence.v1Block-pinned proof that the full bound is deposited in one escrow, binding the immutable EscrowTerms, creation event, identity-registry observation, and a trusted ChannelAssetBinding.
chio.channel.open-intent.v1Both parties propose the channel: identities and keys, currency, immutable bound, dispute tier and duration, close-submission cutoff, and the funding-evidence digest.
chio.channel.funding-acknowledgement.v1The configured funding authority stages the escrow reference from unreserved to the open-intent digest, and signs the version change.
chio.channel.open.v1Binds the open-intent and acknowledgement digests, derives the channel id, and commits the sequence-zero state digest.
chio.channel.reservation.v1The payer's irrevocable one-shot authorization for the exact next receipt-derived state, signed before dispatch.
chio.channel.state.v1One cumulative state per admitted call: the running total, the appended receipt, and the payee signature.
chio.channel.close.v1The final allocation: close kind, final state digest and sequence, intended payee allocation, and the required payee and FROST authorization.
chio.channel.dispute.v1A contested-close challenge: the competing state, its chain proof, reason, and submitter signature.
chio.channel.release-authorization.v1A single-use, durable off-chain publisher record for the release broadcast. It is not an on-chain authorization recognized by ChioEscrow.

Open and Fund

A budget reservation is not channel funding. V1 requires a rail that has already locked the full channel bound, and v1 supports exactly one funded rail: a fully-funded ChioEscrow deposit on the qualified local devnet. Before either party signs the open intent, both verify the chio.channel.funding-evidence.v1 produced from a single block-pinned escrow read. That evidence binds the complete immutable EscrowTerms (capability id, depositor, beneficiary, token, max amount, deadline, operator, and operator-key hash), the deposited, released, and refunded state at the pinned block, the decoded EscrowCreated event, an identity-registry observation proving the named operator is active with that exact key hash, and a trusted ChannelAssetBinding carrying currency, minor-unit decimals, token address, and the settlement-policy digest.

Open validation is exact. The bound must convert cleanly through chio_settle::evm::scale_chio_amount_to_token_minor_units so that terms.maxAmount == deposited == bound_token_base_units; nothing may already be released or refunded; the escrow beneficiary must equal the channel payee and the refund owner must equal the payer; token, currency, both decimal scales, policy digest, chain id, and contract address must all match and round-trip back to the exact MonetaryAmount. The reader uses eth_call at the recorded block tag, then re-reads that block and requires the same block hash; a floating latest result, a non-final block, an inactive operator, or a key mismatch all reject.

The configured funding authority owns one durable funding-reservation registry that serializes each escrow reference through its off-chain lifecycle. Every transition is a versioned external head compare-and-swap followed by an exact local finalize, which is what makes it impossible to reuse one deposit for two channels:

escrow reservation lifecycle
unreserved
  -> opening(open_intent_digest)
  -> open(channel_id)
  -> closing(close_digest)
  -> released | refunded | incident

The final open binds both prior digests and derives a non-cyclic channel id, so an offline verifier can prove terms, a unique reservation acknowledgement, the initial state, and final consent in one direction:

chio-settle/src/channel.rs
channel_id = sha256(canonical_json([
    "chio.channel.id.v1",
    open_intent_digest,
    funding_acknowledgement_digest,
]))

The open record commits a sequence-zero state digest with zero cumulative amount and an empty receipt root. The dispute tier and duration are selected once from the immutable bound and copied into the open record. They never change: no close or dispute recomputes the tier from the cumulative owed, which is what stops a close submitter from shortening the dispute window by choosing a smaller amount.


Reserve Before Each Call

Before any covered tool dispatch, the payer signs a chio.channel.reservation.v1: an irrevocable, one-shot authorization for the exact next receipt-derived state, durably persisted before the tool runs. It binds the reservation id, the channel and open digests, the exact next sequence, the expected prior state digest, the domain-separated digest of the exact request, the provider target and action binding, the trusted receipt-authority digest, a maximum MonetaryAmount with its exact maximum token base units, an expiry, and the expected channel state version and lifecycle fence, all under the payer and channel-authority signatures.

The quoted maximum is derived from bound - cumulative_owed with checked arithmetic and an exact token-base-unit conversion. V1 permits exactly one live reservation per channel, and the store serializes reservation consumption, so there is no payer-selected post-service fork and no latest integer wins rule.

Consent moves before dispatch

A per-state payer signature after service was rejected: it would let a payer consume the tool, refuse to sign the update, and recover value at expiry. The bounded one-shot reservation moves that consent before dispatch instead. No post-service payer signature is requested or consulted in v1, so there is no un-countersigned tail. Zero-charge or denied calls consume no ObligationAtom; their signed receipt still consumes the one-shot reservation and may advance the chain with an unchanged cumulative owed.

Cumulative State

Each admitted call appends one chio.channel.state.v1. Because the consumed pre-dispatch reservation is the payer's authority, a state carries only the payee signature, never a second post-service payer one. The struct below renders the fields the schema binds:

chio-settle/src/channel.rs
pub struct ChannelState {
    pub schema: String, // "chio.channel.state.v1"
    pub channel_id: String,
    pub seq: u64,
    pub prev_state_digest: String,
    pub cumulative_owed: MonetaryAmount,
    pub cumulative_token_base_units: u128,
    pub receipt_root: String, // ordered receipt-id root
    pub receipt_count: u64,
    pub receipt_digest: String, // trusted-kernel receipt
    pub obligation_atom_digest: String,
    pub reservation_digest: String,
    pub actual_charge: MonetaryAmount,
    pub asset_binding_digest: String,
    pub payee_signature: String,
}

The sequence-zero state has zero cumulative amount and an empty receipt root. Every later state must satisfy all of:

  • seq == prior.seq + 1, and the payer-signed reservation names that exact next sequence and prior digest;
  • prev_state_digest == digest(prior_state);
  • the ordered receipt set appends exactly the reservation-bound trusted-kernel receipt;
  • cumulative_owed == prior.cumulative_owed + receipt.cost_charged using checked arithmetic, with the actual charge and its token-base-unit conversion within the signed reservation and the cumulative total within the immutable bound; and
  • one authority-fenced external batch compare-and-swap consumes the reservation and admits the new state.

The same sequence with the same digest is idempotent. The same sequence with a different digest is equivocation: it freezes the channel and cannot replace the already-admitted state. A higher sequence is closeable only when every intermediate reservation, receipt, payee signature, digest link, and authority transition verifies.

One ChannelLifecycleRecord is the single concurrency fence for both service admission and close. New service reservations require the Open lifecycle, so acquiring a close state blocks a later reservation. The record fields and the six variants below are from the spec, while the ChannelLifecycle type name is a rendering:

chio-settle/src/channel.rs
pub enum ChannelLifecycle {
    Open,
    ClosePending,
    Closing,
    Released,
    Refunded,
    Incident,
}

pub struct ChannelLifecycleRecord {
    pub channel_id: String,
    pub lifecycle: ChannelLifecycle,
    pub latest_state_digest: String,
    pub latest_seq: u64,
    pub state_version: u64,       // checked
    pub lifecycle_fence: u64,     // monotonic
    pub live_reservation_id: Option<String>,
    pub operation_id: Option<String>,
}

Close: Cooperative and Contested

The payee may close unilaterally over the newest contiguous reservation-backed, trusted-receipt-derived state. A payer signature after service is neither required nor consulted, and payer disappearance after dispatch cannot remove the closeable balance.

  • Cooperative close. An optional payer close signature can acknowledge the same state, but it cannot reduce the closeable amount.
  • Contested close. The submitter posts the best valid contiguous state and opens the exact dispute duration committed in chio.channel.open.v1. A chio.channel.dispute.v1 can replace it only with a valid descendant chain; an equal-sequence, different-digest submission is equivocation and cannot replace the authority-admitted state.

Before collecting a quorum, begin_close advances the lifecycle from Open to ClosePending, which fails any new service reservation. The close itself is FROST-authorized: the registered ladder action class is channel.close, its co-sign mode is n_of_m, and its consistency model is quorum-required. The proof domain is chio.frost.channel-close.v1, and the authorization binds the exact state digest, sequence, lifecycle fence, and token-base-unit release, so it cannot pick an arbitrary branch or amount. Party signatures and independent endorsements cannot substitute for that group authorization.

Finalization and release broadcast must complete no later than the immutable close_submission_cutoff, which leaves the fixed finality and broadcast margin before the escrow deadline for chain inclusion. Missing the cutoff blocks the release, emits a reconciliation incident, and leaves the escrow remainder for the contract's post-deadline refund; the runtime never attempts a release after expiry.

Lifecycle Diagram

rendering…
Open funds the escrow; Streaming accumulates reservation-backed states; Closing is the FROST-authorized cooperative or contested settle; Settled maps to the Released and Refunded records.

That diagram is a simplified reading aid, not the literal state enum. Streaming renders the Open lifecycle while it accumulates reservations and state updates, and Settled renders the terminal Released / Refunded records. The literal ChannelLifecycleRecord states are the six ChannelLifecycle variants shown above: Open, ClosePending, Closing, Released, Refunded, and Incident.


Settlement and Reconciliation

Release reuses the existing escrow runtime, devnet only. For a non-zero amount below the bound, the close-receipt inclusion proof is passed to the existing prepare_merkle_release with EscrowExecutionAmount::Partial, which prepares partialReleaseWithProofDetailed for the escrow beneficiary; the post-deadline refund returns the remainder. The two transactions are not atomic, and the spec makes no stronger claim.

The intended allocation and the realized allocation are kept strictly apart. final_cumulative_owed is the intended payee allocation, not an observed transfer. The actual released total comes only from a successful, canonically finalized EscrowReleased or EscrowPartialRelease event with matching block-pinned state. After the deadline the contract refund is derived exactly as deployed:

  • actual_refund = deposited - actual_released, proven by a finalized EscrowRefunded event and matching refunded state, never computed as bound - final_cumulative_owed when release failed, paused, missed the cutoff, reorged, or was stranded by key rotation;
  • unpaid_payee_shortfall = final_cumulative_owed - actual_released with checked arithmetic, recorded as an explicit incident rather than silently relabeled as a successful close;
  • after a canonical refund, actual_released + actual_refund must equal the deposited amount, and any actual release above the intended allocation is a security incident.

The protocol is never the payee. Emergency controls and key rotation can change the realized allocation by preventing release before refund, but they cannot rewrite the close's intended allocation.

Devnet limits

V1 is a devnet implementation over a verified, fully-funded existing ChioEscrow deposit on the qualified local devnet. It adds no new Solidity: the live contract does not understand a channel reservation, close digest, or FROST proof, so the reservation-aware gate that guards release is off-chain operational control, not on-chain authority. A beneficiary or the relevant operator, root-publisher, or settlement-key holder can bypass it by calling the existing contract paths directly, including releaseWithSignature. The existing escrow pause, allowlist, and transferable-admin model remain explicit trust assumptions, and channel close finalization and release dispatch stay disabled until the production FROST quorum substrate and its trusted roster, group key, and continuity anchors are live. No mainnet, public-testnet, atomic-close, generic-rail, non-discretionary custody, admin-free, availability, or on-chain FROST-enforcement claim is made.

See Also

  • On-Chain Settlement for the ChioEscrow lifecycle a channel opens against, including the partial-release and refund paths a close reuses.
  • Settlement Rails for the full rail enum and the per-call and off-chain alternatives a channel is chosen against.
  • Clearinghouse for the multilateral netting path that channels sit beside as the bilateral streaming option.
  • Reconciliation & Watchdog for how release and refund observations close out against the exposure ledger.