Esc

    Reference

    Registry

    The registry canister on the Internet Computer lists the nodes and the clients, and a consumer verifies what it publishes.

    The registry is the trust root for membership. It lists which addresses are nodes and which addresses are clients, now and before. It is a canister on the Internet Computer. Nodes and consumers verify what the canister publishes. They never trust the view of the registry that a node gives them.

    Mainnet values

    The SDK and the node have these values built in, in oracle_client::mainnet:

    Value Mainnet
    Canister ID xb5gv-diaaa-aaaaf-qg5qa-cai
    Governance address 0x7FB990Cc1111a00931B6396E5780C79574e36897
    Registry URL https://xb5gv-diaaa-aaaaf-qg5qa-cai.raw.icp0.io/registry.json

    The governance address is the address of the threshold key that signs every registry version and every client list. Pin it in your consumer. Never read it from the document that you fetch.

    What the canister holds

    State Content
    nodes address → endpoints, weight, added_at
    retired address → added_at, removed_at
    clients address → expires_at, added_at
    removed_clients the client addresses that governance removed
    version, effective_from, quorum the metadata of the published document

    An address appears in this state at most once, ever.

    One key per node. The address of the secp256k1 key of a node is the node id. The registry and the SDK key all per-node state by address. There is no separate identity key and no operator field.

    The published document

    The canister publishes one entry for each listed node:

    json
    {
      "version": 7,
      "effective_from": 1754582400,
      "nodes": [
        {
          "address": "0x19e7...ff2a",
          "endpoints": ["wss://eu1.oracle.example/v2/ws"],
          "weight": 1
        }
      ],
      "quorum": { "min_nodes": 3, "recommended": 5 },
      "governance_signature": "0x..."
    }
    Field Meaning
    version Rises by one with each change. A consumer rejects a version that is not strictly newer than the one it holds.
    effective_from When this version was issued, in unix seconds. It is not a freshness signal.
    nodes[].address The node key, which is also the node id.
    nodes[].endpoints 1 to 16 ws:// or wss:// URLs.
    nodes[].weight A weight hint, at least 1. The SDK takes a plain median and does not use it.
    quorum Guidance from governance. The consumer chooses its own quorum.
    governance_signature Threshold ECDSA over the canonical bytes of the registry. Low-S, with an Ethereum recovery byte.

    The document does not name who runs each node. Governance checks operator diversity when it decides whom to list.

    The client list

    Nodes serve prices only to listed clients. Governance lists each client in a second document, the client list. The same threshold key signs it. The canister serves it at /clients.json.

    json
    {
      "version": 12,
      "effective_from": 1754582400,
      "epoch_secs": 60,
      "clients": [{ "address": "0x7f3a...", "expires_at": 1757174400 }],
      "governance_signature": "0x..."
    }
    • The address of the client API key is the client id. It names no customer.
    • A removed client address never comes back.
    • Nodes fetch the list every 30 s by default and check its certificate.
    • A node refuses new sessions when no certified fetch has confirmed the list for 3 hours. It closes every session after 24 hours.
    • Consumers never download the client list. A client change does not change the registry version.

    Rules the canister enforces

    • An address is used once. add_node, replace_node and add_client refuse an address that is, or ever was, a node or a client. A client key never becomes a price-signing key.
    • Never below quorum. The canister refuses a removal that would leave fewer listed nodes than quorum.min_nodes. A replacement keeps the count.
    • Versions only on change. The canister signs a new version only when governance changes something. A failed signing round changes nothing, and governance retries the same call.

    Governance

    Governance is the controller set of the canister, and it is the only caller of every update method but push. The canister rejects every other caller. Nodes call one update method, push, as the anonymous identity, and two queries, get_feeds and get_price_window. See Prices.

    Call Effect
    add_node(entry) Lists a node. The address must be unused. New registry version.
    replace_node(old, new) Rotation. In one version, new takes the place of old with the same endpoints and weight. old is retired.
    remove_node(address) De-lists the node and retires its address. New registry version.
    set_quorum(min_nodes, recommended) Changes the quorum guidance. New registry version.
    add_client, set_client_expiry, remove_client Change the client list. New client-list version.
    republish, republish_clients Sign a document again with no change.
    set_feeds(symbols) Sets the feeds that the canister prices. No new version: the feed list is not in the signed document.
    get_registry, get_clients, get_node_listing Queries. get_node_listing returns added_at and removed_at of an address.

    A node operator sends the node entry, or the two addresses that oracle-node rotate prints, to governance off-chain. Governance makes the call.

    Warning

    Whoever controls the canister controls the trust root. The design requires a multi-principal controller set, such as an SNS or a multisig canister, with a timelock on upgrades and a published wasm hash. The choice of governance body is still open.

    Prices

    The nodes push their latest updates to the canister, each push as one signed batch, and the canister serves the median of the nodes for each priced feed. On-chain prices shows how to read them.

    Call Caller Effect
    push(frames) anonymous A node's batch of latest updates, or single frames. Returns one verdict for each update.
    get_price(feed_id) query One feed's price, resolved at the time of the call.
    get_prices(), get_feeds() query Every feed's price; the feeds and their 32-byte ids.
    set_price_window(secs), get_price_window() governance, query Set and read the price window, 10 to 600 s.
    /prices.json, /frames.json HTTPS Certified. Each feed's price, and the batches and frames that hold each node's latest update. Written when an update arrives.

    The canister runs the Rust SDK's checks on each frame: a listed signer, a sequence that rises, and the clock. inspect_message runs the cheap checks first and lets a push in only when one frame is new and valid, so a replay or junk costs the canister nothing.

    Limit Value Why
    Frame age at push 30 s A node pushes its latest frame, and ingress takes some seconds.
    Clock skew 10 s The canister's clock is consensus time, which can lag wall time.
    Age in the quorum 75 s by default The price window. Governance sets it with set_price_window: about three push intervals. A node that stops pushing stops counting.

    The quorum is the registry's recommended. Each push costs the canister cycles, mostly for its one signature check: about 21M for a batch of 10 updates, against 103M for 10 single frames. At the 30 s heartbeat that is about 1.9T cycles a month for each node.

    The network feed set

    Every node serves the same feeds, so that a quorum can form on each one. Governance publishes them as a third signed document, the feed set, and the canister serves it as the certified /feeds.json. It holds each feed with its sources, as in a node config, the oldest node version that can serve it, and an optional push pace for every node.

    Call Caller Effect
    set_feed_set(json) governance Checks the set, stores it with each name and decimal in one spelling, and signs the next version. It refuses a set that drops a feed priced on-chain, and a push heartbeat above half the price window.
    republish_feeds() governance Signs the set again with no change.
    get_feed_set() query The signed set as JSON.
    /feeds.json HTTPS Certified. The same document.

    A node takes each new version while it runs, in its own slot: two minutes after the publish, then 90 s for each node before it, so that two nodes never change at once. The web proxy follows when the slots have passed. Governance publishes from the repo's feed files with scripts/publish-feeds.sh, which runs every check a node runs before the call.

    How a consumer trusts the registry

    A consumer needs two proofs. The signature proves that a document is genuine. The IC certificate proves that it is current. A consumer needs no Internet Computer agent.

    The signature

    The consumer pins the governance address. It accepts a document only when:

    • the governance signature recovers to the pinned address;
    • the version is strictly newer than the version it holds.

    Thus a node cannot vouch for its own registry, and an old version cannot reinstate a revoked key.

    The certificate and freshness

    A signature does not prove that a document is current, because a cache or a relay can replay an old version. The SDK and the nodes therefore check the IC certificate on every fetch of /registry.json and /clients.json:

    • The certificate must chain to the root key of the network. The mainnet root key is built in.
    • The certificate time must be within 5 minutes of the local clock. A replayed response fails like an outage.
    • Mainnet sources fetch from the raw domain, which passes the response of the canister through unchanged.

    The canister publishes only when something changes, so the age of a document says nothing. Each fetch that verifies confirms the version that it carries. The SDK fetches the registry on a jittered schedule, at most registry_poll_interval (60 s by default) apart. If no fetch confirms the registry for max_registry_age (48 hours by default), every feed reports degraded with the concern registry_stale. The prices are still verified, but the membership can be out of date.

    Note

    GET /v2/registry on a node serves the view of that node. It is a hint, never a trust root.

    Other networks

    HttpRegistrySource::canister(id) fetches from the mainnet raw domain and checks against the mainnet root key. For another network, such as a local network, pass Certification::new(id, root_key) to HttpRegistrySource::new.

    How a rotation reaches the consumer

    A rotation is the replace_node call of governance. In one registry version, the new address takes the place of the old one, at the same endpoint. There is no overlap window. The consumer learns about it from the first frame that it cannot verify, or from a handshake challenge that names the new address.

    1. An unknown signer, or a WrongNode challenge, triggers one fetch from the registry source. The SDK fetches at most once per refresh_interval (10 s by default), so a hostile relay cannot turn the client into a load generator.
    2. refresh_registry adopts the document in place if it is strictly newer and signed by the pinned governance address. There is no reconnect and no loss of feed state. Nodes that the new version de-lists leave the quorum at adoption.
    3. The SDK retries the frame. The quorum seat moves to the new address in the same adoption that drops the old one, so the node never counts twice. The new address starts a fresh replay floor.
    4. The old key is now retired, not unknown. The SDK refuses its frames as RetiredKey and does not fetch again. A steady stream of such frames means that someone uses a stolen key.

    Until the first frame under the new key, the node is out of the quorum of that consumer, as during a restart. Governance staggers rotations so that a consumer never loses two seats at the same time.

    replace_node and remove_node

    Call What the consumer sees
    replace_node(old, new) The seat moves from old to new. Frames from old give RetiredKey. The number of nodes stays the same.
    remove_node(address) The node leaves the quorum at adoption. Frames from it give RetiredKey. The number of nodes drops by one.

    The SDK refuses a new registry version that would leave fewer nodes than its quorum. It keeps the registry that it has and returns an error.

    After subscribe, the connections of the SDK follow the registry. When the SDK adopts a newer version, it connects to added nodes, disconnects removed nodes, and reconnects a node whose endpoint or address changed.

    Stored frames

    The live path verifies a frame only against a listed address. A stored frame, for an audit or a dispute, uses a separate path. verify_historical checks the frame against the listing of its signer from get_node_listing:

    text
    added_at ≤ publish_time_ns < removed_at

    Without removed_at, a stolen key would verify backdated frames forever. The canister states these times. No signature covers them.

    Keys have no expiry. A node key rotates on suspicion of compromise, not on a schedule. A stolen key that nobody notices keeps working until governance replaces or removes the node. Watch for divergence between nodes.