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:
{
"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.
{
"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_nodeandadd_clientrefuse 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
rawdomain, 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.
- An unknown signer, or a
WrongNodechallenge, triggers one fetch from the registry source. The SDK fetches at most once perrefresh_interval(10 s by default), so a hostile relay cannot turn the client into a load generator. refresh_registryadopts 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.- 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.
- The old key is now retired, not unknown. The SDK refuses its frames as
RetiredKeyand 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:
added_at ≤ publish_time_ns < removed_atWithout 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.