Files
universe/scripts/validators.sh
T
Zach KellingandHanzo Dev 8d70cb136f Say where the mainnet validators actually run, and how to roll them
The five Lux mainnet validators are docker containers on the LAN host evo,
started by hand: no compose file, no unit, no script. deploy/lux-mainnet/luxd.yaml
addresses a Service on a cluster this estate no longer has, so the only record of
the flags they carry was the running processes themselves.

scripts/validators.sh carries those flags and rolls the fleet one node at a time,
verifying each is back on the version asked for with four peers before touching
the next -- sample 5 / quorum 4 means two down is the network.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 13:43:45 -07:00

149 lines
7.0 KiB
Bash
Executable File

#!/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