Run a Control Plane
This page takes you from a Star with the CP role to a Control Plane that nodes and clients can find: start star, publish your endpoint and your front’s key, and check it works.
Before anything else
Section titled “Before anything else”The CP role is not granted at registration: every new Star is a Node. During the test stage the founder grants it by hand; later a Star will get it by putting up a stake. See becoming a Control Plane.
This page assumes your Star already has the role.
Everything else needs nobody’s approval: the endpoint list is public, clients poll it themselves, and joining the federation does not need the other members’ consent.
What you need
Section titled “What you need”- The same as for a node: a Linux server, Docker, and a wallet for one transaction.
- A public DNS name and TLS termination in front of the gRPC port.
- ETH on the Control Plane’s signer address. Unlike a node, a Control Plane sends transactions: it publishes time served and relays gas-free operations, and both are paid from the hot signer.
Step 1 — register the Star
Section titled “Step 1 — register the Star”Exactly as for a node — see run a node. You end up with a starId and the role Node.
Step 2 — get the role
Section titled “Step 2 — get the role”Once the role is in place, the program switches over by itself: it reads the role from the chain and starts gRPC on 0.0.0.0:9090. No restart is needed.
Step 3 — fund the signer
Section titled “Step 3 — fund the signer”Copy the signer address from the Star’s page in Stargate and send it some ETH — but only a little. The signer is a hot key on the server, and whatever sits on it is lost if the server is broken into.
A transaction on Arbitrum costs a fraction of a cent. Sending a batch of intervals to the chain takes from about 50 000 to 1 000 000 gas, and at the usual gas price of around 0.02 gwei that is at most 0.00002 ETH. A Control Plane sends a handful of these a day, plus the gas-free operations it relays for clients. 0.005 ETH lasts about a month even if every batch is a heavy one, and usually longer. Top it up when it runs low rather than keeping a large balance.
Keep an eye on the balance all the same: when it reaches zero, the Control Plane stops publishing time served and its nodes stop earning.
Step 4 — configure
Section titled “Step 4 — configure”Most defaults are right. One setting needs your attention:
environment: - BLOCKCHAIN_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc - CHAIN_ID=421614 - REVERSE_PROXY=cp.example.com:443REVERSE_PROXY is the public address this Control Plane gives its federation peers when it introduces itself; it is not written to the chain. Your on-chain endpoint is published separately, by the owner, in step 5. Set both to the same address.
Contract addresses are not configured. At start the program reads every one of them from SingulaProtocol, the network’s directory, so a redeployed contract reaches every Control Plane through a single entry on chain. An address variable such as SUBSCRIBER_REGISTRY_ADDRESS left in a compose file is ignored when the directory disagrees, with a warning in the log. If the directory cannot be read, the program does not start: running on guessed addresses would be worse. SINGULA_PROTOCOL_ADDRESS points at another directory, and off turns the directory off for a local chain, in which case the address variables are used.
Worth knowing:
| Variable | Default | What it does |
|---|---|---|
GRPC_ADDRESS | 0.0.0.0:9090 | Where all four services listen |
ATTESTATION_PUSH_INTERVAL | 4h | How often time served goes to the chain |
ACCESS_SWEEP_INTERVAL | 1h | How often this Control Plane removes lapsed subscribers from the set, on chain and at its own gas cost |
CP_FRONT_ENABLED | true | The front |
CP_FRONT_PORT | 8443 | Where the front listens; clients reach it on 443 through the proxy |
REALITY_KEY_VERSION | 1 | Raising it replaces the front’s key |
BADGER_DIR | /var/lib/star/badger | Local store |
METRICS_HTTP_PORT | 9101 | Port for /status (JSON) and /metrics (Prometheus); 0 turns it off |
METRICS_BIND | 127.0.0.1 | Where that listens. Widen it only behind a firewall or a password |
The network’s rules are not settings: how long credentials last, how much a session must move to be credited, and the limits of the anonymous tier. Every Control Plane reads them from the directory. See limits.
Step 5 — publish the endpoint
Section titled “Step 5 — publish the endpoint”CPEndpointRegistry.setEndpoint(starId, host, port) can be called by the owner, and only while the role is CP.
In Stargate the Control Plane’s card has two buttons for this: Set Endpoint, which takes host or host:port (leave the port out and 0 is stored, meaning “the protocol’s default”), and Clear Endpoint.
Once the transaction lands you appear in getActiveEndpoints(), and nodes and clients pick you up on their next poll.
Step 6 — publish the front’s key
Section titled “Step 6 — publish the front’s key”Every Control Plane runs a front: a REALITY entrance that leads to its own gRPC and nowhere else, for clients on networks that block the endpoint. Its settings are fixed by the protocol, so any client can reach any Control Plane’s front with what it already has. Only the key is your own.
The key is derived from the signer key and REALITY_KEY_VERSION. There is nothing new to keep, and the key survives a move to another machine. The Control Plane prints it at every start:
REALITY public key (version 1): Vfv6lG2gjLQSbkLJdcH72Eo7zW2LrNRhAyrvU_YPJGQPublish it with the REALITY key action on the Control Plane’s card in Stargate, or with CPEndpointRegistry.setRealityKey(starId, key), passing the 32 bytes the base64url text decodes to. Clients read the key from there. If you raise the version, publish the new key.
The front listens on CP_FRONT_PORT. Clients come to port 443 with the server name account.adobe.com, so route that name to the front and pass TLS through untouched; REALITY has to see the ClientHello itself. With Traefik:
labels: - traefik.tcp.routers.cp-front.rule=HostSNI(`account.adobe.com`) - traefik.tcp.routers.cp-front.entrypoints=websecure - traefik.tcp.routers.cp-front.tls.passthrough=true - traefik.tcp.routers.cp-front.service=cp-front - traefik.tcp.services.cp-front.loadbalancer.server.port=8443No node preset uses that name, so a proxy that shares 443 between a Control Plane and a node can route both by name without a clash.
Step 7 — check it
Section titled “Step 7 — check it”grpcurl <host>:<port> singula.admin.v1.AdminService/GetHealthreturnsSERVING.- You appear in
getActiveEndpoints()on chain. - Stargate fills with live sessions and connected nodes.
- Within a few hours of serving traffic,
Pushedevents from your signer appear onEpochAttestations.
Behind a reverse proxy
Section titled “Behind a reverse proxy”The program speaks plain gRPC, and TLS is terminated in front of it. The proxy talks HTTP/2 to it without TLS, which is what h2c means.
cp.example.com { reverse_proxy h2c://127.0.0.1:9090}services: traefik: image: traefik:v3.1 restart: unless-stopped command: - --providers.docker=true - --providers.docker.exposedbydefault=false - --entrypoints.websecure.address=:443 - --entrypoints.web.address=:80 - --certificatesresolvers.le.acme.tlschallenge=true - --certificatesresolvers.le.acme.email=ops@example.com - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json ports: ["443:443", "80:80"] volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./letsencrypt:/letsencrypt
star: image: singulanetwork/star:alpha container_name: singula-cp restart: unless-stopped env_file: .env labels: - traefik.enable=true - traefik.http.routers.cp.rule=Host(`cp.example.com`) - traefik.http.routers.cp.entrypoints=websecure - traefik.http.routers.cp.tls.certresolver=le - traefik.http.services.cp.loadbalancer.server.port=9090 - traefik.http.services.cp.loadbalancer.server.scheme=h2cservices: star: image: singulanetwork/star:alpha container_name: singula-cp restart: unless-stopped env_file: .env expose: ["9090"] networks: [edge]
haproxy: image: haproxy:2.9-alpine restart: unless-stopped depends_on: [star] ports: ["443:443"] volumes: - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro - ./certs/cp.pem:/etc/ssl/cp.pem:ro # cat fullchain.pem privkey.pem > cp.pem networks: [edge]
networks: edge: {}
# haproxy.cfg# global# log stdout format raw local0# defaults# mode http# timeout connect 5s# timeout client 1h# timeout server 1h# frontend grpc_in# bind *:443 ssl crt /etc/ssl/cp.pem alpn h2# default_backend grpc_cp# backend grpc_cp# server cp star:9090 proto h2Keep the timeouts long: client streams stay open for hours.
What to watch
Section titled “What to watch”A Control Plane reports on itself at the same two addresses as a node: /status returns JSON and /metrics the Prometheus format, both on METRICS_HTTP_PORT.
curl -s localhost:9101/status | jqThe answer has the epoch, how many nodes are connected and how many are waiting to be registered, how many client sessions are open, how many intervals are waiting to be sent and how long since anything reached the chain, plus uptime, CPU and memory. The info block names the signer address and the starId.
Worth an alert of their own:
singula_cp_attestation_pushes_totalat zero. Nothing this Control Plane has served has ever been paid for.- The signer’s ETH balance. At zero, publishing stops and your nodes stop earning.
- Disk space for BadgerDB.
- No
Pushedevents onEpochAttestationswhile traffic flows: intervals are being dropped instead of submitted. relay errorlines in the log — see troubleshooting.
