Operate
Rotate a node key
Replace a node's signing key with oracle-node rotate and the replace_node call that governance runs, and what consumers see during the swap.
A rotation is one canister call, not a registry edit. The operator makes the next key and sends both addresses to governance. Governance calls replace_node, which swaps the addresses atomically. The operator then moves the new key into place and restarts the node.
When to rotate
Rotate a key when you think that it leaked. Keys do not expire, and there is no rotation schedule. Each rotation is a governance call, and the median across nodes is the defence against one bad key. See Key management.
How a rotation works
A node has one secret seed. The address of its secp256k1 key is the node's id. A rotation is a new seed whose address takes the place of the old one.
replace_node(old, new) on the registry canister:
- is atomic: in one registry version,
newtakes the place ofold, with the same endpoints and weight; - refuses a
newaddress that is, or ever was, a node or a client; - retires
old: the canister records itsremoved_atand never lists it again, for anyone; - keeps the number of listed nodes, so the quorum rule always holds;
- publishes a new registry version with a new governance signature.
There is no overlap window. To a consumer, the new address is a new node in the old node's quorum seat.
Steps
1. Make the next key
oracle-node rotate --dir node1rotate reads node1/config.toml and opens the live key node1/signing.key. It then writes the next key to node1/signing.key.next, encrypted with the same passphrase. Nothing on the node changes yet. The node keeps signing with the old key.
For a node from sudo oracle-node init, run rotate as the service user, so that the service user owns the new key:
sudo -u oracle-node oracle-node rotate --dir /var/lib/oracle-node| Flag | Meaning | Default |
|---|---|---|
--dir DIR |
The directory that init --out wrote: signing.key and config.toml. |
required |
--canister ID |
The registry canister id for the printed call. | registry.canister_id from the config |
--passphrase-file FILE |
A 0600 file with the passphrase. One passphrase opens the live key and encrypts the next one. |
signing.passphrase_file from the config, then a prompt |
rotate prints the old address, the new address, the new ed25519 public key, and the call for governance:
icp canister call <CANISTER_ID> replace_node '("0xOLD…", "0xNEW…")' -n icThe addresses are checksummed, so a typo fails at the canister and does not burn an address. The network is local when the config has a registry.root_key, and ic otherwise.
rotate refuses to write when signing.key.next already exists. That file is a rotation that never finished, and the canister may already list its key.
If the passphrase is encrypted with systemd-creds, give rotate the plain passphrase with --passphrase-file. See Keep the passphrase encrypted.
2. Update ed25519 venues first
A venue that verifies ed25519, such as tapray-perps, can delay a new publisher key by a day. Register the new ed25519 key there before step 3.
3. Governance runs replace_node
Send both addresses to governance. Governance runs the printed call. The canister refuses an address that it has ever seen.
From this moment the registry lists only the new address. The running node still signs with the old key, and consumers refuse those frames as signed by a retired key. Do step 4 at once.
4. Move the key into place and restart
mv node1/signing.key.next node1/signing.key
oracle-node start node1/config.tomlUnder systemd, restart the service after the mv:
sudo -u oracle-node mv /var/lib/oracle-node/signing.key.next /var/lib/oracle-node/signing.key
sudo systemctl restart oracle-nodeAt startup the node checks that the registry lists its address. A node started with the new key before step 3 waits. It does not sign or serve until the registry lists the new address, and it checks every 30 seconds.
Stagger rotations
During a rotation, the node is out of each consumer's quorum for a short time. See What consumers see. The other nodes carry the feed, as during a restart.
Two rotations at the same time take two seats from each consumer's quorum. Governance and operators therefore stagger rotations, so that a consumer never loses two seats at once.
What consumers see
The node, the registry and each consumer switch at different instants. A consumer finds out about the rotation from the first frame that it cannot verify, or from a handshake challenge that names the new address.
- An unknown signer, or a challenge from an unexpected address (
WrongNode), makes the consumer fetch the registry once. The SDK fetches at most once for eachrefresh_interval, 10 s by default. Anyone who can inject a frame can cause a fetch, so the limit protects the registry from a hostile relay. - The consumer adopts the new document if it is strictly newer and signed by the pinned governance address. Nodes that the new version removes leave the quorum at once, not when their last update ages out.
- The consumer tries the frame again. The quorum seat moves to the new address in the same step that drops the old one, so the node never counts twice. The new address starts a new replay floor. That is safe, because the new key never signed before.
- The old key is now retired, not unknown. The consumer refuses its frames as
RetiredKeyand does not fetch again. A few such frames right after a rotation were in flight. A steady stream of them is a stolen key in use.
The node leaves a consumer's quorum at the governance call (operator step 3). It comes back when its first frame under the new key, or its first challenge, makes that consumer fetch the registry. This is a one-node gap for each consumer, not an outage. The design accepts it.
The project's end-to-end test checks this path. A client that runs through a rotation adopts the new registry with at most one fetch. It accepts no untrusted frame, and it sees no replay.
Stored frames
The old address stays on the canister as retired. get_node_listing(address) returns its added_at and removed_at. A stored frame verifies against the listing that covered its publish time:
added_at ≤ publish_time_ns < removed_atThus a rotation does not invalidate history, and a stolen old key cannot sign new frames that verify. The SDK's verify_historical does this check. The live path never uses it.