Fresh Join
Use Automatic for the supported install path. Switch to Manual when you need to build from source, manage TLS yourself, or run each setup stage directly.
-
Provision The Validator Host
Minimum production requirement: use at least this host size before running Fresh Join.
- Platform
- Linux amd64
- CPU
- 4 vCPU
- Memory
- 8 GB RAM
- Storage
- 120 GB NVMe
Linux arm64 is also supported. macOS builds are for local development, not production validators.
-
Run Fresh Join
Run this on the validator host. It initializes the validator, restores or syncs chain data, registers the operator for approval, and starts the local service.
curl -fsSL https://shielded-vote.nyc3.digitaloceanspaces.com/join.sh | bashThe helper server runs with
svotedon local port1317. Wallets submit shares to a public HTTPS URL, so the installer asks how to expose that helper API:- Skip Caddy. Use this if you already handle the public HTTPS certificate with a load balancer or reverse proxy.
-
Custom domain + Caddy. The script
installs Caddy, gets a Let's Encrypt certificate, and
proxies
https://your-domaintolocalhost:1317. -
Auto sslip.io + Caddy. The script builds
a public HTTPS URL from the host's detected IPv4, installs
Caddy, and proxies it to
localhost:1317.
When registration finishes, contact Valar Group and tell them your validator is pending approval.
-
Publish The Vote Server URL
Add the public HTTPS helper URL to
vote_servers[]inprod/dynamic-voting-config.json. If Fresh Join configured Caddy, use the URL it prints. If you skipped Caddy, use the public URL from your own HTTPS proxy. Add one JSON object withurlandlabel, then open a PR from GitHub's edit flow. This can be done while the validator is still pending approval. Edit voting config. For a concrete example, see this merged voting config PR. -
Back Up Validator Identity
Back up
config/priv_validator_key.json,config/node_key.json,keyring-test/,pallas.*, andea.*encrypted off-host. Keep the validator signing key live on exactly one host.
-
Install Source Build Requirements
Minimum production requirement: use at least this host size before building and joining a validator manually. Use Linux for production validators; macOS source builds are for local development.
- Platform
- Linux amd64
- CPU
- 4 vCPU
- Memory
- 8 GB RAM
- Storage
- 120 GB NVMe
sudo apt-get update sudo apt-get install -y curl git jq lz4 ca-certificates build-essential pkg-config clang lld command -v mise >/dev/null 2>&1 || curl https://mise.run | sh export PATH="$HOME/.local/bin:$PATH" -
Build The Active Chain Version
Build from the release tag reported by the current seed node. Running a different app version can cause replay or app-hash failures after snapshot restore.
git clone https://github.com/valargroup/vote-sdk.git cd vote-sdk mise install export PATH="$HOME/go/bin:$PATH" VOTING_CONFIG_URL="${VOTING_CONFIG_URL:-https://voting.valargroup.dev/prod/dynamic-voting-config.json}" VOTING_CONFIG=$(curl -fsSL "$VOTING_CONFIG_URL") SEED_URL=$(echo "$VOTING_CONFIG" | jq -r '.vote_servers[0].url') NODE_INFO=$(curl -fsSL "${SEED_URL%/}/cosmos/base/tendermint/v1beta1/node_info") NODE_ID=$(echo "$NODE_INFO" | jq -r '.default_node_info.default_node_id // .default_node_info.id') LISTEN_ADDR=$(echo "$NODE_INFO" | jq -r '.default_node_info.listen_addr') CHAIN_BINARY_VERSION=$(echo "$NODE_INFO" | jq -r '.application_version.version') SEED_HOST=$(echo "$SEED_URL" | sed -E 's|^https?://||; s|:[0-9]+$||; s|/.*||') P2P_PORT=$(echo "$LISTEN_ADDR" | sed -E 's|.*:([0-9]+)$|\1|') PERSISTENT_PEERS="${NODE_ID}@${SEED_HOST}:${P2P_PORT:-26656}" git fetch --tags git checkout "$CHAIN_BINARY_VERSION" mise install mise run install LOCAL_SVOTED_VERSION=$(svoted version 2>/dev/null | tr -d '[:space:]') test "$LOCAL_SVOTED_VERSION" = "$CHAIN_BINARY_VERSION"mise run installis the source-build task; it is also available asmise run build:install. -
Initialize State And Restore Snapshot
This creates a fresh validator home, installs canonical genesis, and restores the latest snapshot when one is published. Set
MONIKERto the validator name admins should see in the join queue. If no snapshot metadata exists, sync from genesis.MONIKER="my-validator" HOME_DIR="$HOME/.svoted" rm -rf "$HOME_DIR" svoted init "$MONIKER" --chain-id zvote-1 --home "$HOME_DIR" GENESIS_TMP=$(mktemp) LIVE_GENESIS_TMP=$(mktemp) curl -fsSL -o "$GENESIS_TMP" https://shielded-vote.nyc3.digitaloceanspaces.com/genesis.json if curl -fsSL -o "$LIVE_GENESIS_TMP" "${SEED_URL%/}/shielded-vote/v1/genesis" 2>/dev/null; then cmp -s "$GENESIS_TMP" "$LIVE_GENESIS_TMP" || cp "$LIVE_GENESIS_TMP" "$GENESIS_TMP" fi cp "$GENESIS_TMP" "$HOME_DIR/config/genesis.json" svoted genesis validate-genesis --home "$HOME_DIR" SNAPSHOT_META=$(mktemp) SNAPSHOT_ARCHIVE=$(mktemp) VALIDATOR_STATE=$(mktemp) if curl -fsSL -o "$SNAPSHOT_META" https://snapshots.valargroup.org/latest.json; then test "$(jq -r '.chain_id' "$SNAPSHOT_META")" = "zvote-1" SNAPSHOT_URL=$(jq -r '.url' "$SNAPSHOT_META") SNAPSHOT_SUM=$(jq -r '.checksum' "$SNAPSHOT_META") curl -fL -o "$SNAPSHOT_ARCHIVE" "$SNAPSHOT_URL" ACTUAL_SUM=$(sha256sum "$SNAPSHOT_ARCHIVE" | awk '{print $1}') test "$ACTUAL_SUM" = "$SNAPSHOT_SUM" cp "$HOME_DIR/data/priv_validator_state.json" "$VALIDATOR_STATE" rm -rf "$HOME_DIR/data" lz4 -dc "$SNAPSHOT_ARCHIVE" | tar -C "$HOME_DIR" -xf - cp "$VALIDATOR_STATE" "$HOME_DIR/data/priv_validator_state.json" rm -rf "$HOME_DIR/data/cs.wal" else echo "No snapshot metadata is available; the node will sync from genesis." fi -
Generate Keys And Configure The Node
This creates validator, Pallas, and EA keys; then it points CometBFT at the seed peer and enables the REST helper on local port
1317.svoted init-validator-keys --home "$HOME_DIR" VALIDATOR_ADDR=$(svoted keys show validator -a --keyring-backend test --home "$HOME_DIR") VALIDATOR_VALOPER=$(svoted keys show validator --bech val -a --keyring-backend test --home "$HOME_DIR") CONFIG_TOML="$HOME_DIR/config/config.toml" APP_TOML="$HOME_DIR/config/app.toml" sed -i.bak "s|persistent_peers = \"\"|persistent_peers = \"${PERSISTENT_PEERS}\"|" "$CONFIG_TOML" sed -i.bak '/\[api\]/,/\[.*\]/ s/enable = false/enable = true/' "$APP_TOML" sed -i.bak '/\[api\]/,/\[.*\]/ s/enabled-unsafe-cors = false/enabled-unsafe-cors = true/' "$APP_TOML" sed -i.bak "s|\\$HOME/.svoted|${HOME_DIR}|g" "$APP_TOML" rm -f "${CONFIG_TOML}.bak" "${APP_TOML}.bak" cat >> "$APP_TOML" <<'HELPERCFG' [helper] disable = false api_token = "" db_path = "" chain_api_port = 1317 max_concurrent_proofs = 8 HELPERCFG -
Expose HTTPS To The Helper API
Wallets need a public HTTPS URL that proxies to
localhost:1317. Use your own load balancer or configure Caddy. This step must leaveVALIDATOR_URLset to the public HTTPS URL that should be published for wallets.VALIDATOR_DOMAIN="validator.example.org" VALIDATOR_URL="https://${VALIDATOR_DOMAIN}" sudo apt-get install -y caddy sudo tee /etc/caddy/Caddyfile > /dev/null <<EOF ${VALIDATOR_DOMAIN} { reverse_proxy localhost:1317 } EOF sudo caddy validate --config /etc/caddy/Caddyfile sudo systemctl restart caddyIf TLS is handled upstream, skip the Caddy block and set
VALIDATOR_URLdirectly before continuing.VALIDATOR_URL="https://validator.example.org" -
Register For Funding Approval
Run this after
VALIDATOR_URLis set, whether you configured Caddy or use upstream TLS. This is the funding and approval request: it sends your operator address, public URL, andMONIKERto the primary admin API.SVOTE_ADMIN_URL="${SVOTE_ADMIN_URL:-https://vote-chain-primary.valargroup.org}" TIMESTAMP=$(date +%s) REG_PAYLOAD=$(jq -nc \ --arg oa "$VALIDATOR_ADDR" \ --arg u "${VALIDATOR_URL:-}" \ --arg m "$MONIKER" \ --argjson ts "$TIMESTAMP" \ '{operator_address:$oa,url:$u,moniker:$m,timestamp:$ts}') SIG_JSON=$(svoted sign-arbitrary "$REG_PAYLOAD" --from validator --keyring-backend test --home "$HOME_DIR") SIG=$(echo "$SIG_JSON" | jq -r '.signature') PUB_KEY=$(echo "$SIG_JSON" | jq -r '.pub_key') REG_BODY=$(jq -nc \ --arg oa "$VALIDATOR_ADDR" \ --arg u "${VALIDATOR_URL:-}" \ --arg m "$MONIKER" \ --argjson ts "$TIMESTAMP" \ --arg s "$SIG" \ --arg pk "$PUB_KEY" \ '{operator_address:$oa,url:$u,moniker:$m,timestamp:$ts,signature:$s,pub_key:$pk}') curl -fsSL -X POST "${SVOTE_ADMIN_URL%/}/api/register-validator" \ -H "Content-Type: application/json" \ -d "$REG_BODY" | jqWhen the response shows
pendingorregistered, message the voting admin with the validator name and operator address so they can approve and fund it. -
Install Cosmovisor
These Linux steps use Cosmovisor, matching the default in
join.sh. Cosmovisor runs the current chain binary and switches to the new binary at a scheduled upgrade height. Run this from the source checkout used above. The shared installer verifies the Cosmovisor download checksum and stages the currentsvotedbinary under$HOME_DIR/cosmovisor/genesis/bin/svoted.INSTALL_DIR="${GOBIN:-$HOME/go/bin}" SVOTED_BIN=$(command -v svoted) ( set -euo pipefail export SVOTE_HOME="$HOME_DIR" export SVOTE_INSTALL_DIR="$INSTALL_DIR" source scripts/_chain_upgrade_common.sh svote_upgrade_resolve_paths COSMOVISOR_TMP=$(mktemp -d) trap 'rm -rf "$COSMOVISOR_TMP"' EXIT svote_upgrade_install_cosmovisor "$COSMOVISOR_TMP" svote_upgrade_stage_binary "$SVOTED_BIN" "$GENESIS_BIN" ) -
Install The Wrapper Service
The wrapper starts
svotedthrough Cosmovisor, waits for sync and vote-manager funding, runscreate-val-tx, then writesjoin-completeafter bonding. The service enables checksum-required upgrade downloads and restarts the chain binary after an upgrade.As in
join.sh,UNSAFE_SKIP_BACKUP=trueskips Cosmovisor's local pre-upgrade chain data copy to avoid duplicate storage use. Recovery requires an external backup or a fresh Valar snapshot. Keep validator identity files encrypted off-host.WRAPPER_BIN="${INSTALL_DIR}/svoted-wrapper.sh" cp scripts/svoted-wrapper.sh "$WRAPPER_BIN" chmod +x "$WRAPPER_BIN" sudo tee /etc/systemd/system/svoted.service > /dev/null <<EOF [Unit] Description=Shielded-Vote validator (${MONIKER}) After=network.target [Service] Type=simple User=$(whoami) Environment="PATH=${INSTALL_DIR}:/usr/local/bin:/usr/bin:/bin" "SVOTE_HOME=${HOME_DIR}" "VALIDATOR_ADDR=${VALIDATOR_ADDR}" "VALIDATOR_VALOPER=${VALIDATOR_VALOPER}" "MONIKER=${MONIKER}" "SVOTE_INSTALL_DIR=${INSTALL_DIR}" "SVOTED_BIN=${SVOTED_BIN}" Environment="SVOTE_CHAIN_ID=zvote-1" "SVOTE_UPGRADE_MODE=cosmovisor" Environment="DAEMON_HOME=${HOME_DIR}" "DAEMON_NAME=svoted" "COSMOVISOR_BIN=${INSTALL_DIR}/cosmovisor" Environment="DAEMON_RESTART_AFTER_UPGRADE=true" "DAEMON_ALLOW_DOWNLOAD_BINARIES=true" "DAEMON_DOWNLOAD_MUST_HAVE_CHECKSUM=true" "UNSAFE_SKIP_BACKUP=true" ExecStart=${WRAPPER_BIN} Restart=on-failure RestartSec=5 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable svoted sudo systemctl restart svoted -
Verify, Publish, And Back Up
Confirm the service process tree shows Cosmovisor with one
svotedchild. Watch the service until the node is synced and the join queue shows the operator. After bonding, publish the public helper URL tovote_servers[]and back up validator identity files encrypted off-host.sudo systemctl status svoted --no-pager journalctl -u svoted -f svoted status --home "$HOME_DIR" | jq '{network: .node_info.network, height: .sync_info.latest_block_height, catching_up: .sync_info.catching_up}' curl -fsS http://127.0.0.1:1317/cosmos/base/tendermint/v1beta1/node_info | jq '.default_node_info.network' curl -fsS "${VALIDATOR_URL}/shielded-vote/v1/genesis" > /dev/null && echo "public helper OK"Back up
config/priv_validator_key.json,config/node_key.json,keyring-test/,pallas.*, andea.*. Keep the validator signing key live on exactly one host. Edit voting config.