chio-link 0.1.2

Oracle runtime for Chio cross-currency budget enforcement
Documentation
# chio-link architecture

## Overview

`chio-link` turns Chainlink and Pyth price feeds into `ExchangeRate`s the rest
of Chio can use for cross-currency budget enforcement. It sits at an
untrusted edge: every backend response is pair-matched and freshness-checked
before it can enter the cache, and every outbound RPC/HTTP call is gated by a
typed `HttpEgressContract` before it leaves the process. `chio-kernel` embeds
`ChioLinkOracle` behind the `PriceOracle` trait during request evaluation;
`chio-metering` enforces budgets against the units this crate converts.

## Module map

| Path | Responsibility |
|------|----------------|
| `src/lib.rs` | `ExchangeRate`, `PriceOracleError`, the `PriceOracle`/`OracleBackend` traits, and `ChioLinkOracle` (backend selection, cache, operator controls, health reports). |
| `src/config.rs` | `PriceOracleConfig`, `PairConfig`, `PairPolicy`, `OperatorConfig`, `PairRuntimeOverride`, `ChainlinkNetworkConfig`; config validation and `build_default_egress_contract`. |
| `src/cache.rs` | `PriceCache`: latest-rate storage per pair, rolling observation window, TWAP averaging. |
| `src/chainlink.rs` / `src/chainlink_disabled.rs` | `ChainlinkFeedReader` (`OracleBackend` for Chainlink). Feature-gated: `chainlink.rs` reads `AggregatorV3Interface.latestRoundData`/`decimals` over a contract-pinned `alloy` transport; `chainlink_disabled.rs` is the `web3`-off stub that always errors. |
| `src/pyth.rs` | `PythHermesClient` (`OracleBackend` for Pyth): fetches Hermes `latest_price_feeds`, validates the returned feed id, normalizes decimal price/confidence into a rate ratio. |
| `src/sequencer.rs` / `src/sequencer_disabled.rs` | L2 sequencer-uptime reads (`read_sequencer_status`) against a second `AggregatorV3Interface` binding (no `decimals`). Feature-gated like the Chainlink backend. |
| `src/circuit_breaker.rs` | `divergence_bps` and `ensure_within_threshold`: basis-point divergence between two rates for the same pair. |
| `src/convert.rs` | `minor_units_for_currency`, `convert_units`, `convert_supported_units`: fixed-point cross-currency conversion with basis-point margin and ceiling rounding. |
| `src/monitor.rs` | `OracleRuntimeReport`, `ChainHealthReport`, `PairHealthReport`, `OracleAlert` and their status/severity enums. |
| `src/reports.rs` | Private classification helpers that turn a resolved rate or error into a `PairHealthStatus`/`OracleAlert`, and a chain status into an `OracleAlert`. |
| `src/control.rs` | `ChioLinkControlState`: an append-only audit trail (before/after JSON) for operator actions, under the `chio.link.control-state.v1`/`control-trace.v1` schemas. |

## Rate resolution

1. `pair_config` looks up the requested base/quote in `PriceOracleConfig`,
   re-validating the whole config on every call.
2. `enforce_operator_controls` fails closed if the global pause is active,
   the pair's runtime override is disabled, its chain is disabled, or (when
   the chain has a `sequencer_uptime_feed`) the L2 sequencer is down or still
   inside its recovery grace period.
3. `resolve_cached_rate` returns a cached rate if fresh, folding recent
   observations into a TWAP when `PairPolicy::twap_enabled`. On a stale entry
   it tries `degraded_rate_if_allowed`, which extends `max_age_seconds` and
   adds margin instead of failing, but only when `DegradedModePolicy::enabled`
   and the entry is no older than `max_age_seconds + max_stale_age_seconds`.
4. On a cache miss, `fetch_authoritative_rate` reads the primary backend (or
   the pair override's forced backend). If a fallback is configured, allowed,
   and supported by the pair, it is also read and cross-checked against the
   primary with `circuit_breaker::ensure_within_threshold`; divergence beyond
   the threshold fails closed with `CircuitBreakerTripped` even though a
   valid primary rate exists. If the primary read fails, the fallback is used
   unchecked.
5. Every backend response passes through `read_validated_backend_rate`:
   `ensure_matches_pair` (exact base/quote match) and `ensure_fresh`
   (non-zero denominator, `updated_at` not in the future, age within
   `max_age_seconds`) before it can enter the cache.
6. `ExchangeRate::to_conversion_evidence` re-checks freshness and builds an
   `OracleConversionEvidence` stamped with `schema =
   chio.oracle-conversion-evidence.v1` and `authority = chio_link_runtime_v1`,
   unsigned, for a downstream signer and
   `chio-web3::anchors::validate_oracle_conversion_evidence` on the
   receiving end.

## Invariants and failure modes

- Fail closed by default: operator pause, disabled chain, sequencer
  down/recovering, stale price, pair mismatch, and circuit-breaker
  divergence all reject the call instead of returning a best-effort rate.
  Degraded mode is the one explicit, policy-gated exception, and it is off by
  default (`DegradedModePolicy::disabled`).
- `ChioLinkOracle::new`/`new_with_backends` reject a config whose declared
  `primary`/`fallback` backend kind does not match the constructed backend's
  `OracleBackend::kind()`.
- The `web3` feature gates all live Chainlink and sequencer reads. With it
  off, `chainlink_disabled`/`sequencer_disabled` return
  `PriceOracleError::UnsupportedBackend` (or, for a configured sequencer
  feed, always error), and `build_backend` refuses to construct a
  Chainlink-primary/fallback oracle at all.
- Every RPC and HTTP dispatch (Chainlink JSON-RPC, Pyth Hermes, sequencer
  uptime) routes through a typed `HttpEgressContract`: scheme/authority
  allow-listing, loopback/link-local/IPv6-ULA denial, a pinned DNS resolver
  enforced at connect, a bounded redirect chain, and a maximum response size.
  `PriceOracleConfig::validate` also enforces the contract against the
  configured Pyth and chain RPC URLs at config-load time.
- Config types derive `#[serde(deny_unknown_fields)]`, so an unrecognized
  field in operator or pair config fails deserialization instead of being
  silently dropped.
- `control::ChioLinkControlState` is a data structure only; nothing in
  `ChioLinkOracle` writes to it automatically. An integrator that needs an
  audit trail must call `record_global_pause`/`record_chain_enabled`/
  `record_pair_override` itself alongside the matching `ChioLinkOracle`
  mutator.

## Dependencies

- `chio-core` - via `web3::anchors` (a `chio-web3` re-export) supplies
  `OracleConversionEvidence`, `CHIO_LINK_ORACLE_AUTHORITY`, and
  `CHIO_ORACLE_CONVERSION_EVIDENCE_SCHEMA`; via `web3::settlement` supplies
  `CHIO_LINK_CONTROL_STATE_SCHEMA`/`CHIO_LINK_CONTROL_TRACE_SCHEMA` for
  `control.rs`.
- `chio-egress-contract` (`reqwest-egress` feature) - `HttpEgressContract`
  plus `client_builder_with_contract`/`send_with_contract`, enforced on every
  outbound dispatch.
- `alloy-*` (`web3` feature) - JSON-RPC client, contract bindings (`sol!`),
  and primitives for the on-chain Chainlink and sequencer-uptime reads,
  wrapped in a custom `tower::Service` (`ContractJsonRpcTransport`) so every
  request routes through the egress contract instead of `alloy`'s default
  transport.
- `reqwest` - the Pyth Hermes HTTP client and the transport `alloy` is built
  on.
- `tokio` - `RwLock` around the cache and operator config for concurrent
  reads with exclusive writes.

## Extension points

- `OracleBackend` - implement to add a price source beyond Chainlink/Pyth;
  `ChioLinkOracle` only requires `kind()` and `read_rate()`.
- `PriceOracle` - the trait consumers (`chio-kernel`) program against instead
  of depending on `ChioLinkOracle` directly.