Esc

    Overview

    Trust model

    What a node signature proves, what the quorum protects against, and the failures that the median across nodes cannot catch.

    Read this page before you build on the oracle. It states what the oracle guarantees and where those guarantees stop.

    What a signature proves

    A signature proves authorship: node X stated this value at this time. It proves nothing about correctness. The signed payload contains num_sources, but nothing binds that number to reality. A node that never connected to a venue can sign num_sources: 5 all day. There is no verifiable computation.

    The defence against a node that lies or fails is not cryptographic. The defence is the median across N nodes, plus monitoring of the divergence between them. Two rules follow:

    • A consumer with a quorum of one has zero protection against a bad node.
    • "Signed" means "attributable". It does not mean "trustworthy".

    A signed update is also a bearer token that never expires by itself. A relay can stop the new updates and send an old one again. The old update passes every cryptographic check. Only the staleness and sequence checks of the consumer catch this. See Verification failure modes.

    What quorum means for a consumer

    The quorum is the number of distinct nodes that must report a feed before the SDK gives a usable price. You set it on the SDK builder with .quorum(n). The default is 3.

    For each feed, the SDK keeps the latest accepted update from each node and resolves the feed as follows:

    1. It excludes each node that reports the feed as halted, and adds the concern node_reported_halted.
    2. It ignores each update older than max_age (2 s by default). A node that stops sending stops counting.
    3. If fewer live nodes remain than the quorum, the status is halted with the concern insufficient_nodes.
    4. The price is the median of the node prices.
    5. conf is the larger of the median node conf and half the spread between the highest and lowest node price.
    6. If the spread is larger than max_divergence_bps (50 bp by default), the status is degraded with the concern node_divergence.
    7. If a node reports its feed as degraded, the status is degraded with the concern node_reported_degraded.

    Step 5 is the important one. A node that disagrees cannot move the median. Its disagreement makes conf wider. A consumer that acts on conf therefore reacts correctly with no extra logic.

    The registry also publishes quorum.min_nodes and quorum.recommended. These values are guidance from governance. The SDK does not apply them. You choose your own quorum. The SDK refuses to build if the registry lists fewer nodes than your quorum. It also refuses a new registry version that would leave fewer nodes than your quorum.

    Why a quorum below 3 is refused

    OracleClientBuilder::build refuses a quorum below three by default. To allow it, you must call i_understand_this_provides_no_protection_against_a_bad_node(). The web proxy has the same rule: it needs allow_low_quorum = true for a quorum below three.

    Quorum Effect of one bad node
    1 The bad node sets the price. You only know who sent it.
    2 The median of two prices is their midpoint. The bad node moves the price by half of its error. conf becomes wider, but the price still moves.
    3 or more One bad node cannot move the median. Its disagreement makes conf wider.

    The escape hatch exists for local development against one node. In production it means that you trust the honesty of one machine completely. This is the mistake that is most likely during integration and least likely to be fixed later.

    Warning

    The SDK protects you against a node with a bad key or a bad operator. It does not protect you against a fault that every node shares. See Correlated failure.

    Trust when you use the web proxy

    The web proxy runs the SDK, with its signature checks and its quorum. A web client then trusts the proxy through TLS. The node signatures stop at the proxy, so the operator of the proxy can change prices. A consumer that must not trust an operator uses the SDK directly.

    Encryption on /v2/ws limits who can read the stream, not who can pass it on. A listed client can forward the plaintext, and the node signatures make it verifiable. That is a contractual matter, not a cryptographic one.

    Verification failure modes

    These mistakes break the security of a client that verifies frames. The SDK prevents each one. If you write your own verifier, you must prevent them too.

    Mistake Consequence What the SDK does
    Recover the signer but do not check the registry Total bypass. Anyone signs anything. No method returns a price without the registry check.
    Trust a registry that a node serves The node authorises itself. Only the canister document counts, checked against a governance address that you pin.
    Trust the canister because it is on chain A single controller can upgrade it to publish anything. Nothing in the SDK. The protection is multi-principal controllers, timelocked upgrades and a published wasm hash.
    Verify bytes that you rebuilt, not the received payload False rejects, then someone "fixes" them by skipping verification. It hashes only the received payload bytes.
    Parse publish_time_ns as a JavaScript number Nanosecond times are near 1.75e18, far past 2^53. Precision is lost and replay checks break. Every 64-bit field in JSON is a string.
    Accept high-S signatures Two valid byte forms for one update. Deduplication breaks. It rejects high-S signatures.
    No staleness limit A relay replays old signed updates, and every signature verifies. It rejects an update outside [now − max_age, now + max_skew].
    Treat a retired key as live A stolen key stays usable after the rotation that was to revoke it. Live frames verify only against listed addresses. A retired key gives RetiredKey.
    Verify a stored frame against the current registry Every rotation invalidates history. verify_historical checks the listing that covered the publish time.
    Refresh the registry on each unknown signer, with no limit Anyone who can inject a frame forces a fetch per frame. At most one fetch per refresh_interval (10 s by default).
    Key client state by registry index Indices move on each refresh. Replay floors and quorum seats move to the wrong node. All state for a node is keyed by its address.

    The SDK checks each update in this order:

    1. It recovers the signer and rejects a high-S signature.
    2. It looks up the address in the registry. An unknown address causes one registry fetch, then one retry. A retired address is rejected as RetiredKey with no fetch.
    3. It rejects a seq that does not rise above the last one for that node and feed.
    4. It rejects a publish_time_ns outside [now − max_age, now + max_skew]. The defaults are 2 s and 500 ms.
    5. It rejects an observation_time_ns later than publish_time_ns.

    Correlated failure

    The median of N nodes defends against independent faults. Several things in this system are not independent. This is the real limit of the architecture.

    The sources are correlated. Binance, Bybit and OKX are not three observations of the market. They are three views of mostly the same USDT flow, arbitraged against each other and anchored to Binance. The number of independent sources is closer to 2 or 3 than to 5. If Binance halts or misprints, the other USDT venues drift with it. The num_sources field overstates robustness to a reader who takes it literally.

    The nodes run the same software. The median defends against key compromise, operator compromise and machine failure. It does not defend against a bug in oracle-agg. Every node then signs the same wrong price with valid signatures. This is the largest residual risk in the design. The partial defences are:

    • oracle-agg stays a pure function with an adversarial replay suite.
    • One node can run an aggregator from an independent implementation, as a canary.
    • Monitoring of the divergence between nodes detects many faults, but it cannot detect this class.

    Failure domains collapse quietly. Five nodes in five regions of one cloud are one node in a costume. So are five nodes that one operator deploys from one CI with one set of credentials. Governance checks operator diversity when it lists nodes.

    Venue feeds degrade together, at the worst time. Venues throttle, lag and drop connections hardest during high volatility. That is when the feed matters most. Expect the staleness filter to drop sources during the events that you most need to price. For this reason the degraded path is a tested path, not an exception path.

    Market-structure pitfalls

    • Thin books. A median resists one manipulated venue at any weight. It does not resist three of five venues that are thin enough to push cheaply. That is the normal state for assets outside the majors. Depth weighting helps a little. The choice of feeds helps more.
    • USDT circularity. CRYPTO.USDT/USD must never come from a venue that quotes in USDT. During a real depeg, the Coinbase and Kraken USDT-USD books also become thin, exactly when every other feed depends on them. See USDT is not USD.
    • Reflexivity. If a perp DEX prices off this oracle and its liquidations move the venue books, the oracle partly reads its own output. The effect is bounded for majors. It is dangerous for small assets.
    • A halt is not automatically safe. A halted feed stops the liquidation engine of a lending protocol. That can be worse than a slightly wrong price. For this reason the aggregation prefers a wider conf to a halt. You must decide which failure mode your protocol can accept.

    Operational pitfalls

    • Wall-clock jumps. publish_time_ns comes from the wall clock, but seq and the replay checks assume that time moves forward. The node takes its receipt and publish times from one monotonic clock that it re-anchors to the wall clock each second. A clock behind wall time steps forward. A clock ahead of it runs at 90% speed until it matches.
    • Restarts reset counters. A seq that restarts at zero makes every consumer refuse the node as a replay for hours. The node seeds seq from its clock at startup, so a restart continues above the previous run.
    • Conflation shows state, not transitions. A slow subscriber sees the latest update of each feed, not every update. A HALTED blip shorter than one second can disappear behind the next OK frame. Every frame carries complete state, and the 1 s heartbeat makes a longer halt impossible to miss. Write your consumer to read state. Do not accumulate transitions. If an audit needs every transition, that needs a separate durable log.
    • Nodes always disagree a little. They have different network paths and see the market at different instants. A small spread between nodes is normal. Watch spread_bps on each resolved price.
    • The firehose. A subscription to all feeds ("*") grows with the catalogue, and it becomes a denial of service that you cause yourself. Subscribe to the feeds that you need.