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://vote.fra1.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[]indynamic-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. -
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.org/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 svote-1 --home "$HOME_DIR" GENESIS_TMP=$(mktemp) LIVE_GENESIS_TMP=$(mktemp) curl -fsSL -o "$GENESIS_TMP" https://vote.fra1.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")" = "svote-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 The Wrapper Service
The wrapper starts
svoted, waits for sync and vote-manager funding, runscreate-val-tx, then writesjoin-completeafter bonding.INSTALL_DIR="${GOBIN:-$HOME/go/bin}" WRAPPER_BIN="${INSTALL_DIR}/svoted-wrapper.sh" SVOTED_BIN=$(command -v svoted) 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}" 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
Watch the service until the node is synced and the join queue shows the operator. After bonding, publish the public helper URL to
vote_servers[]and back up validator identity files encrypted off-host.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.