#!/usr/bin/env bash # The five validators of Lux mainnet, where they actually run, and the one way # to move them to a new luxd. # # They are NOT in a Kubernetes cluster. deploy/lux-mainnet/luxd.yaml describes a # StatefulSet against luxd-headless.lux-mainnet.svc.cluster.local:9630, and no # such Service exists on the k3s cluster this estate has: the DOKS cluster that # file was written for is gone. The validators are five docker containers on the # LAN host `evo`, started by hand, with no compose file, no unit and no script — # which is why finding them took an afternoon. This file is that missing script. # # Ports are positional: node i answers HTTP on 9730+10i and stakes on 9731+10i, # with its ledger at /var/lib/luxd/mainnet/data-i. Every node bootstraps off the # staking ports of the other four. Consensus is sample 5 / quorum 4, so four # must be up to decide anything: roll ONE at a time, and only when the one # before it is back, bootstrapped and connected to the other four. # # scripts/validators.sh list # scripts/validators.sh roll 1.37.1 set -euo pipefail host=${LUXD_HOST:-evo} root=/var/lib/luxd/mainnet repo=ghcr.io/luxfi/node chain=0x17871 # 96369, Lux mainnet C-Chain # index:name — the name is the node's identity, the index is its port and ledger. # Three of the five answered to hanzo-*/zoo-* names for a while; they are five # validators of one network (all mount the same genesis) and are named for it. nodes=(0:lux-mainnet-0 1:lux-mainnet-1 2:lux-mainnet-2 3:lux-mainnet-3 4:lux-mainnet-4) http() { echo $((9730 + 10 * $1)); } stake() { echo $((9731 + 10 * $1)); } # beacons are the staking ports of every node except this one. beacons() { local self=$1 i out= for i in 0 1 2 3 4; do [ "$i" = "$self" ] && continue out="${out:+$out,}127.0.0.1:$(stake "$i")" done echo "$out" } # run recreates node $1 on image $2, keeping its ledger. Every flag the running # fleet carries is here; nothing is defaulted silently. run() { local i=$1 name=$2 image=$3 ssh "$host" docker rm -f "$name" 2>/dev/null || true ssh "$host" docker run -d --name "$name" --network host --restart unless-stopped \ -v "$root/genesis.json:/genesis/genesis.json:ro" \ -v "$root/chainconf:/chainconf:ro" \ -v "$root/data-$i:/data" \ "$image" /bin/sh -c "' mkdir -p /data/plugins cp /luxd/build/plugins/* /data/plugins/ 2>/dev/null || true exec /luxd/build/luxd --network-id=1 --genesis-file=/genesis/genesis.json \ --chain-config-dir=/chainconf --data-dir=/data --db-type=zapdb --plugin-dir=/data/plugins \ --staking-tls-cert-file=/data/staking/staker.crt \ --staking-tls-key-file=/data/staking/staker.key \ --staking-signer-key-file=/data/staking/signer.key \ --http-host=0.0.0.0 --http-port=$(http "$i") --http-allowed-hosts=\"*\" --staking-port=$(stake "$i") \ --sybil-protection-enabled=true \ --consensus-sample-size=5 --consensus-quorum-size=4 \ --consensus-preference-quorum-size=4 --consensus-confidence-quorum-size=4 \ --consensus-commit-threshold=2 \ --network-require-validator-to-connect=true --network-allow-private-ips=true \ --bootstrap-nodes=\"$(beacons "$i")\" --skip-bootstrap=false --track-all-chains=true \ --public-ip=127.0.0.1 --api-admin-enabled=true --index-enabled=true --log-level=info '" >/dev/null } # ask prints "version peers head bootstrapped prefix" for the node on port $1. # # It reads both generations because a roll leaves the fleet mixed. Up to # luxfi/constants v1.6.3 a chain hung off /v1/bc and info was JSON-RPC at # /v1/info; from v1.6.4 the segment is /v1/chain and info is typed operations # under /v1/info/ops. The prefix a node answers on is the fact this rolls for, # so it is reported, not assumed. ask() { local p=$1 v peers head boot prefix v=$(ssh "$host" "curl -s -m 6 http://127.0.0.1:$p/v1/info/ops/node/version" 2>/dev/null | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') if [ -n "$v" ]; then prefix=chain peers=$(ssh "$host" "curl -s -m 6 http://127.0.0.1:$p/v1/info/ops/peers" 2>/dev/null | sed -n 's/.*"numPeers":"\([0-9]*\)".*/\1/p') boot=$(ssh "$host" "curl -s -m 6 'http://127.0.0.1:$p/v1/info/ops/chain/bootstrapped?chain=C'" 2>/dev/null | sed -n 's/.*"isBootstrapped":\([a-z]*\).*/\1/p') else prefix=bc v=$(ssh "$host" "curl -s -m 6 -X POST -H 'content-type: application/json' --data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"info.getNodeVersion\",\"params\":[]}' http://127.0.0.1:$p/v1/info" 2>/dev/null | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') peers=$(ssh "$host" "curl -s -m 6 -X POST -H 'content-type: application/json' --data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"info.peers\",\"params\":[]}' http://127.0.0.1:$p/v1/info" 2>/dev/null | sed -n 's/.*"numPeers":"\([0-9]*\)".*/\1/p') boot=$(ssh "$host" "curl -s -m 6 -X POST -H 'content-type: application/json' --data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"info.isBootstrapped\",\"params\":{\"chain\":\"C\"}}' http://127.0.0.1:$p/v1/info" 2>/dev/null | sed -n 's/.*"isBootstrapped":\([a-z]*\).*/\1/p') fi head=$(ssh "$host" "curl -s -m 6 -X POST -H 'content-type: application/json' --data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_blockNumber\",\"params\":[]}' http://127.0.0.1:$p/v1/$prefix/C/rpc" 2>/dev/null | sed -n 's/.*"result":"\([^"]*\)".*/\1/p') echo "${v:-down} ${peers:-0} ${head:--} ${boot:-false} ${prefix}" } list() { printf '%-16s %-6s %-18s %-6s %-10s %-6s %s\n' NODE PORT VERSION PEERS HEAD BOOT PREFIX local e i name for e in "${nodes[@]}"; do i=${e%%:*}; name=${e##*:} # shellcheck disable=SC2046 printf '%-16s %-6s %-18s %-6s %-10s %-6s %s\n' "$name" "$(http "$i")" $(ask "$(http "$i")") done } # roll moves every node to $1, one at a time. A node that does not come back on # the new prefix with four peers stops the roll where it stands: four of five is # quorum exactly, so a second unhealthy node is the network. roll() { local want=${1#v} image e i name p line v peers prefix waited image="$repo:v$want" ssh "$host" docker pull -q "$image" >/dev/null for e in "${nodes[@]}"; do i=${e%%:*}; name=${e##*:}; p=$(http "$i") echo "== $name ($p) before: $(ask "$p")" run "$i" "$name" "$image" waited=0 while [ "$waited" -lt 300 ]; do line=$(ask "$p"); v=${line%% *} case "$v" in luxd/"$want") break ;; esac sleep 10; waited=$((waited + 10)) done line=$(ask "$p") set -- $line; v=$1; peers=$2; prefix=$5 echo "== $name after: $line" if [ "$v" != "luxd/$want" ] || [ "$prefix" != chain ] || [ "${peers:-0}" -lt 4 ]; then echo "STOP: $name did not come back as luxd/$want on /v1/chain with four peers" >&2 exit 1 fi ssh "$host" "curl -s -m 6 -X POST -H 'content-type: application/json' --data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_chainId\",\"params\":[]}' http://127.0.0.1:$p/v1/chain/C/rpc" | grep -q "$chain" || { echo "STOP: $name does not answer $chain on /v1/chain/C/rpc" >&2; exit 1; } done list } case "${1:-list}" in list) list ;; roll) roll "${2:?usage: validators.sh roll X.Y.Z}" ;; *) echo "usage: validators.sh [list | roll X.Y.Z]" >&2; exit 2 ;; esac