Relay Utils
relay_utils is the command-line tool that drives the operational steps of a Symbiotic Relay
network — the work that sits between deploying the Relay contracts
and running the Relay sidecar:
- Network operators use it to commit the genesis validator set that bootstraps the network.
- Validators/operators use it to generate their signing keys, register them in
KeyRegistry, and check whether they made it into the validator set.
What you need
Every command below uses the same handful of values. Here is exactly what each one is and where to get it:
| Value | What it is | Where to get it |
|---|---|---|
<ValSetDriver address> | The network's on-chain config contract — the single source of truth the CLI reads everything else from. | The network operator gives it to you. (After deployment it is written into the network's my-relay-deploy.toml.) |
<driver_chain_id> | Chain ID where ValSetDriver is deployed. | From the network operator, alongside the driver address. |
<vpp_chain_id> | Chain ID where the network's VotingPowerProvider lives (often the same as the driver chain). | From the network operator, or read it: cast call <ValSetDriver> "getVotingPowerProviders()((uint64,address)[])" --rpc-url <rpc>. |
<rpc_url> (and <rpc1>,<rpc2>,…) | A JSON-RPC endpoint for each chain you transact on (driver, VPP, settlement chains). | Your own node or a provider (e.g. Alchemy). Supported chains and Symbiotic Core addresses: Addresses. |
<tag> (key tags) | Which validator keys the network requires — you generate and register one per tag. | Ask the network operator, or read on-chain: cast call <ValSetDriver> "getRequiredKeyTags()(uint8[])" --rpc-url <rpc> (the header-signing tag is getRequiredHeaderKeyTag()). |
<operator_private_key> | Your operator EOA key — signs the on-chain transactions and pays gas. | Your own key. Fund the address with native gas on every chain you transact on. |
Operator path
Joining an existing network, in order:
- Find the required key tags — they live in the driver's config (
getRequiredKeyTags); you generate one relay key per tag → Generate and manage keys. - Register each key in
KeyRegistry→ Register a key. - Register your operator in the
VotingPowerProvider→ Register as an operator. On auto-deploy networks this also creates your vault. - Opt into your vault and deposit stake — Core opt-ins + the curator's allocation limits — so your voting power is active.
- Run the sidecar → Relay Off-Chain, then confirm with Check operator status.
Generate and manage keys
Keys live in a local keystore (--path, default ./keystore.jks, unlocked with --password).
Generate an EVM key for paying gas on a chain, or import an existing one:
# generate a fresh EVM key for the voting-power-provider chain
utils keys add --generate --evm --chain-id <vpp_chain_id> --password <pwd>
# or import an existing private key
utils keys add --evm --chain-id <vpp_chain_id> --private-key 0x... --password <pwd>Generate one relay signing key per key tag the network requires (ask the network operator which tags are required):
utils keys add --relay --key-tag <tag> --generate --password <pwd>Inspect and clean up:
utils keys list --password <pwd>
utils keys remove --evm --chain-id <vpp_chain_id> --password <pwd>Register a key in KeyRegistry
Each relay key must be registered on-chain so the network can attribute signatures to your operator. Run once per required key tag:
utils operator register-key \
--driver.address <ValSetDriver address> \
--driver.chainid <driver_chain_id> \
--voting-provider-chain-id <vpp_chain_id> \
--chains <rpc_url> \
--key-tag <tag> \
--secret-keys <vpp_chain_id>:<operator_private_key> \
--password <pwd>This builds the proof of key ownership and calls KeyRegistry.setKey(...). You don't pass the
KeyRegistry address — relay_utils resolves it from the ValSetDriver. To see it directly (e.g. to
inspect the contract on a block explorer), read it off the driver:
cast call <ValSetDriver address> "getKeysProvider()((uint64,address))" --rpc-url <rpc_url>The returned (chainId, address) is the chain and address of the network's KeyRegistry.
Register as an operator
Register your operator in the network's VotingPowerProvider. This adds you to the network's operator
set; once you also hold the required keys and stake, you become eligible for the derived validator set.
It calls VotingPowerProvider.registerOperator():
utils operator register-operator \
--driver.address <ValSetDriver address> \
--driver.chainid <driver_chain_id> \
--voting-provider-chain-id <vpp_chain_id> \
--chains <rpc_url> \
--secret-keys <vpp_chain_id>:<operator_private_key>As with key registration, the VotingPowerProvider address is resolved from the ValSetDriver — you
don't pass it. To see it directly (e.g. to register through a Safe, or to look up your vault below):
cast call <ValSetDriver address> "getVotingPowerProviders()((uint64,address)[])" --rpc-url <rpc_url>Verify the registration:
cast call <VotingPowerProvider address> "isOperatorRegistered(address)(bool)" <operator_address> --rpc-url <rpc_url>To let a third party submit the transaction (gasless), generate an EIP-712 signature with
utils operator register-operator-with-signature and hand the artifact to the submitter.
Commit the genesis validator set
A network operator runs this once to bootstrap the network: it derives the first validator set and commits the genesis header to every settlement chain. Until genesis is committed, sidecars have no starting snapshot to agree on.
utils network generate-genesis \
--driver.address <ValSetDriver address> \
--driver.chainid <driver_chain_id> \
--chains <rpc1>,<rpc2>,... \
--secret-keys <chain_id>:<key>,... \
--commitOmit --commit for a dry run — it prints the derived header without sending transactions, which is
the fastest way to sanity-check your ValSetDriver configuration. Inspect the live network with:
utils network info \
--driver.address <ValSetDriver address> \
--driver.chainid <driver_chain_id> \
--chains <rpc1>,<rpc2>,...Check operator status
Confirm your operator is registered, included, and carries voting power:
utils operator info \
--driver.address <ValSetDriver address> \
--driver.chainid <driver_chain_id> \
--voting-provider-chain-id <vpp_chain_id> \
--chains <rpc_url>If you registered keys and deposited stake but still show zero voting power, the missing piece is almost always a vault delegator limit — see Manage Allocations.
