Files
universe/k8s/lux-devnet/luxd-statefulset.yaml
T

262 lines
15 KiB
YAML

# luxd StatefulSet — lux-devnet (5 validators, C-Chain EVM 96367).
#
# Rendered from live 2026-07-28 (same tracking contract as ../lux-testnet and
# ../lux-mainnet: lux-operator and lux-operator-devnet are held at 0/0, so
# nothing reconciles the owning LuxNetwork CR and this file is the durable
# source for the object that actually moves pods). updateStrategy is OnDelete:
# spec changes take effect per-pod only when a pod is deleted, one at a time,
# gated on eth_blockNumber tip parity.
#
# readinessProbe — TIP-FRESHNESS, not isBootstrapped. This is the public-RPC
# load-balancer gate (svc/luxd -> api-gateway -> api.lux-dev.network), and
# isBootstrapped(C) is TRUE on a tip-frozen node: on 2026-07-28 luxd-2 sat
# wedged at height 1789 for 5h (proposervm "failed to fetch preferred block"),
# stayed Ready, and kube-proxy round-robined public reads onto it —
# api.lux-dev.network alternated height 1996/1789 (nonce 27/26) between
# consecutive requests, a split-brain that faked out acceptance probes.
# The probe now asserts the one thing a public caller needs: this node's
# last-accepted C-Chain block is <= 300s old. A frozen node self-evicts from
# the Service; a node catching up (re-executing old blocks) stays out until
# near-tip; a node whose C-Chain is down fails the curl outright. devnet runs
# chain-traffic continuously, so >300s of no accepted blocks IS a fault, and
# an honestly-empty Service (gateway 503) beats a coherent-looking lie.
#
# metadata.ownerReferences pins this object to LuxNetwork/luxd (lux.cloud/v1)
# by uid. If that CR is ever deleted+recreated, re-render this file (the uid
# changes); omitting the block here would ORPHAN the live object on apply.
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app.kubernetes.io/component: validator
app.kubernetes.io/instance: luxd
app.kubernetes.io/managed-by: hanzo-operator
app.kubernetes.io/name: luxd
app.kubernetes.io/part-of: lux-network
app.kubernetes.io/version: v1.34.9
name: luxd
namespace: lux-devnet
ownerReferences:
- apiVersion: lux.cloud/v1
blockOwnerDeletion: true
controller: true
kind: LuxNetwork
name: luxd
uid: b8f5e54d-2431-461e-aeba-77d752c63fc2
spec:
minReadySeconds: 10
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
podManagementPolicy: Parallel
replicas: 5
revisionHistoryLimit: 10
selector:
matchLabels:
app.kubernetes.io/instance: luxd
app.kubernetes.io/name: luxd
serviceName: luxd-headless
template:
metadata:
annotations:
kubectl.kubernetes.io/restartedAt: '2026-07-26T23:11:09-07:00'
labels:
app.kubernetes.io/component: validator
app.kubernetes.io/instance: luxd
app.kubernetes.io/managed-by: hanzo-operator
app.kubernetes.io/name: luxd
app.kubernetes.io/part-of: lux-network
app.kubernetes.io/version: v1.34.9
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- luxd
topologyKey: kubernetes.io/hostname
containers:
- command:
- /bin/sh
- -c
- "#!/bin/sh\nset -eu\n\n# GENERATED BY lux-operator from the LuxNetwork CR. Do not edit by hand \u2014\n# edit the CR and let the operator re-render. One way, deterministic.\n\nPOD_INDEX=\"${HOSTNAME##*-}\"\nPUBLIC_IP=\"$(hostname -i | awk '{print $1}')\"\necho \"[boot] luxd validator ordinal=${POD_INDEX} ip=${PUBLIC_IP} network-id=3\"\n\n# WipeOnRecreate=fullDB: clear chain state for a clean re-bootstrap from genesis.\necho \"[boot] WIPE fullDB: /data/db /data/chainData /data/genesis.bytes\"\necho '[boot] wipe disabled (state-preserving, matches testnet)'\n\nBOOTSTRAP_NODES=\"\"\ni=0\nwhile [ \"$i\" -lt 5 ]; do\n if [ \"$i\" != \"$POD_INDEX\" ]; then\n PEER='luxd'-${i}.luxd-headless.lux-devnet.svc.cluster.local:9651\n if [ -n \"$BOOTSTRAP_NODES\" ]; then BOOTSTRAP_NODES=\"${BOOTSTRAP_NODES},${PEER}\"; else BOOTSTRAP_NODES=\"${PEER}\"; fi\n fi\n i=$((i+1))\ndone\n\n# Install this pod's per-ordinal staking material. Clearing stale chain\n# data on a key change keeps a rotated validator from carrying a stale DB.\nmkdir -p '/data/staking'\nSRC_CRT='/staking-keys'/staker-${POD_INDEX}.crt\nSRC_KEY='/staking-keys'/staker-${POD_INDEX}.key\nSRC_SIGNER='/staking-keys'/signer-${POD_INDEX}.key\nMOUNTED_FP=\"$(sha256sum \"$SRC_CRT\" 2>/dev/null | awk '{print $1}' || echo none)\"\nSAVED_FP=\"$(cat '/data'/.staking-fingerprint 2>/dev/null || echo '')\"\nif [ \"$MOUNTED_FP\" != \"$SAVED_FP\" ]; then\n echo \"[boot] staking key changed (${SAVED_FP:-none} -> ${MOUNTED_FP}); clearing stale chain data\"\n rm -rf '/data'/db '/data'/chainData '/data'/LOCK 2>/dev/null || true\n echo \"$MOUNTED_FP\" > '/data'/.staking-fingerprint\nfi\ncp \"$SRC_CRT\" '/data/staking'/staker.crt\ncp \"$SRC_KEY\" '/data/staking'/staker.key\ncp \"$SRC_SIGNER\" '/data/staking'/signer.key\nchmod 600 '/data/staking'/staker.key '/data/staking'/signer.key\n\n# Seed per-chain EVM upgrade.json (precompile-activation schedules). The\n# C-Chain blob turns on the 0x9999 DEX-settlement precompile. Idempotent:\n# same bytes every boot. upgrade.json is config, NOT post-bootstrap chain\n# state, so it carries no triedb-root hazard.\nmkdir -p '/data/configs/chains/C' && cp '/upgrades/cchain-upgrade.json' '/data/configs/chains/C'/upgrade.json\nmkdir -p '/data/configs/chains/hanzo' && cp '/upgrades/brand-upgrade.json' '/data/configs/chains/hanzo'/upgrade.json\nmkdir -p '/data/configs/chains/osage' && cp '/upgrades/brand-upgrade.json' '/data/configs/chains/osage'/upgrade.json\nmkdir -p '/data/configs/chains/pars' && cp '/upgrades/brand-upgrade.json' '/data/configs/chains/pars'/upgrade.json\nmkdir -p '/data/configs/chains/spc' && cp '/upgrades/brand-upgrade.json' '/data/configs/chains/spc'/upgrade.json\nmkdir -p '/data/configs/chains/zoo' && cp '/upgrades/brand-upgrade.json' '/data/configs/chains/zoo'/upgrade.json\n\nmkdir -p '/data/configs/chains'\nif [ -f '/aliases/aliases.json' ]; then cp '/aliases/aliases.json' '/data/configs/chains/aliases.json'; echo '[boot] chain aliases installed'; fi\n\n# Install the node image's baked VM plugins (the image is the source of truth).\nmkdir -p '/data/plugins'\ncp '/luxd/build/plugins'/* '/data/plugins'/ 2>/dev/null || true\n\n# Symlink shared VM binaries so one plugin serves several blockchain IDs.\nif [ -e '/data/plugins/mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6' ] && [ ! -e '/data/plugins/ag3GReYPNuSR17rUP8acMdZipQBikdXNRKDyFszAysmy3vDXE' ]; then ln -sf '/data/plugins/mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6' '/data/plugins/ag3GReYPNuSR17rUP8acMdZipQBikdXNRKDyFszAysmy3vDXE'; fi\n\nif [ ! -f '/data/.zap-native-migrated' ]; then\n echo '[migrate] zap-native marker absent -> wiping /data/db /data/chainData for clean re-bootstrap'\n rm -rf '/data/db' '/data/chainData'\n mkdir -p '/data'\n : > '/data/.zap-native-migrated'\n echo '[migrate] wipe complete; marker set (future restarts no-wipe)'\nelse\n echo '[migrate] zap-native marker present -> NO wipe (durable-fix catch-up path)'\nfi\n\nexec /luxd/build/luxd \\\n --network-id=3 \\\n --http-host=0.0.0.0 \\\n --http-port=9650 \\\n --http-allowed-hosts=* \\\n --staking-port=9651 \\\n --data-dir=/data \\\n --genesis-file=/genesis/genesis.json \\\n --db-type=zapdb \\\n --index-enabled=true \\\n --api-admin-enabled=true \\\n --api-metrics-enabled=true \\\n --api-keystore-enabled=true \\\n --log-level=info \\\n --plugin-dir=/data/plugins \\\n --staking-tls-cert-file=/data/staking/staker.crt \\\n --staking-tls-key-file=/data/staking/staker.key \\\n --staking-signer-key-file=/data/staking/signer.key \\\n --network-compression-type=zstd \\\n --consensus-sample-size=5 \\\n --consensus-quorum-size=4 \\\n --consensus-commit-threshold=2 \\\n --consensus-concurrent-repolls=2 \\\n --sybil-protection-enabled=true \\\n --skip-bootstrap=true \\\n --network-require-validator-to-connect=false \\\n --network-allow-private-ips=true \\\n --track-all-chains \\\n --chain-config-dir=/data/configs/chains \\\n --dex-validator=true \\\n --upgrade-file-content=eyJkdXJhbmdvVGltZXN0YW1wIjowLCJxdWFzYXJUaW1lc3RhbXAiOjAsImZvcnR1bmFUaW1lc3RhbXAiOjAsImdyYW5pdGVUaW1lc3RhbXAiOjB9 \\\n --public-ip=\"$PUBLIC_IP\" \\\n --bootstrap-nodes=\"$BOOTSTRAP_NODES\"\n"
env:
- name: ZAP_RPC_LISTEN
value: :9632
- name: LUX_NETWORK_ID
value: '3'
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: REPLICATE_S3_ACCESS_KEY
valueFrom:
secretKeyRef:
key: accessKeyId
name: replicate-s3-creds
- name: REPLICATE_S3_SECRET_KEY
valueFrom:
secretKeyRef:
key: secretAccessKey
name: replicate-s3-creds
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
key: accessKeyId
name: replicate-s3-creds
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
key: secretAccessKey
name: replicate-s3-creds
- name: AWS_REGION
value: us-east-1
- name: AWS_EC2_METADATA_DISABLED
value: 'true'
- name: LUX_CONSENSUS_VIEW_CHANGE
value: 'true'
- name: VIEW_CHANGE
value: 'true'
image: ghcr.io/luxfi/node:v1.36.125@sha256:f65281b532a6b2294b83f57610cad2d292a12cf5c85232986bd0b54979d76862
imagePullPolicy: Always
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- sleep 5
livenessProbe:
failureThreshold: 5
httpGet:
path: /v1/health/liveness
port: 9650
scheme: HTTP
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 5
name: luxd
ports:
- containerPort: 9632
name: zap-rpc
protocol: TCP
- containerPort: 9650
name: http
protocol: TCP
- containerPort: 9651
name: staking
protocol: TCP
- containerPort: 9090
name: metrics
protocol: TCP
readinessProbe:
exec:
command:
- /bin/sh
- -c
- 'TS=$(curl -sf -m 4 -X POST http://127.0.0.1:9650/v1/chain/C/rpc -H ''Content-Type: application/json'' -d ''{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["latest",false]}'' | sed -n ''s/.*"timestamp":"0x\([0-9a-f]*\)".*/\1/p''); [ -n "$TS" ] && [ $(( $(date +%s) - 0x$TS )) -le 300 ]'
failureThreshold: 3
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 8
resources:
limits:
cpu: '6'
memory: 12Gi
requests:
cpu: '2'
memory: 4Gi
startupProbe:
failureThreshold: 60
httpGet:
path: /v1/health/liveness
port: 9650
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /data
name: data
- mountPath: /genesis
name: genesis
readOnly: true
- mountPath: /staking-keys
name: staking-keys
readOnly: true
- mountPath: /aliases
name: chain-aliases
readOnly: true
- mountPath: /upgrades
name: chain-upgrades
readOnly: true
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: ghcr-luxfi
initContainers:
- args:
- "set -eu\nPOD_INDEX=${HOSTNAME##*-}\necho \"[GATE] peer-wait for $HOSTNAME (minPeers=2 timeout=30s)\"\nPREFIX=\"luxd\"\nSUFFIX=\"luxd-headless.lux-devnet.svc.cluster.local\"\nPORT=9651\nMIN_PEERS=2\nGATE_TIMEOUT=30\nGATE_INTERVAL=5\nGATE_START=$(date +%s)\nwhile true; do\n REACHABLE=0\n if [ \"$POD_INDEX\" != \"0\" ]; then\n nc -z -w 2 \"${PREFIX}-0.${SUFFIX}\" \"$PORT\" 2>/dev/null && REACHABLE=$((REACHABLE+1)) || true\n fi\n if [ \"$POD_INDEX\" != \"1\" ]; then\n nc -z -w 2 \"${PREFIX}-1.${SUFFIX}\" \"$PORT\" 2>/dev/null && REACHABLE=$((REACHABLE+1)) || true\n fi\n if [ \"$POD_INDEX\" != \"2\" ]; then\n nc -z -w 2 \"${PREFIX}-2.${SUFFIX}\" \"$PORT\" 2>/dev/null && REACHABLE=$((REACHABLE+1)) || true\n fi\n if [ \"$POD_INDEX\" != \"3\" ]; then\n nc -z -w 2 \"${PREFIX}-3.${SUFFIX}\" \"$PORT\" 2>/dev/null && REACHABLE=$((REACHABLE+1)) || true\n fi\n if [ \"$POD_INDEX\" != \"4\" ]; then\n nc -z -w 2 \"${PREFIX}-4.${SUFFIX}\" \"$PORT\" 2>/dev/null && REACHABLE=$((REACHABLE+1)) || true\n fi\n if [ \"$REACHABLE\" -ge \"$MIN_PEERS\" ]; then\n echo \"[GATE] $REACHABLE peers reachable \u2014 proceeding\"\n break\n fi\n ELAPSED=$(( $(date +%s) - GATE_START ))\n if [ \"$ELAPSED\" -ge \"$GATE_TIMEOUT\" ]; then\n echo \"[GATE] timeout with $REACHABLE peers \u2014 proceeding (StartAnyway)\"\n break\n fi\n echo \"[GATE] $REACHABLE/$MIN_PEERS peers (${ELAPSED}/${GATE_TIMEOUT}s) \u2014 waiting\"\n sleep \"$GATE_INTERVAL\"\ndone\n"
command:
- /bin/sh
- -c
env:
- name: REPLICATE_S3_PATH
value: devnet/zaprepl-v4-recover
- name: LUX_CONSENSUS_VIEW_CHANGE
value: 'false'
image: ghcr.io/luxfi/node:v1.36.125@sha256:f65281b532a6b2294b83f57610cad2d292a12cf5c85232986bd0b54979d76862
imagePullPolicy: IfNotPresent
name: startup-gate
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
restartPolicy: Always
schedulerName: default-scheduler
securityContext:
fsGroup: 1000
runAsGroup: 1000
runAsUser: 1000
terminationGracePeriodSeconds: 150
volumes:
- configMap:
defaultMode: 420
name: luxd-genesis
name: genesis
- name: staking-keys
secret:
defaultMode: 420
secretName: luxd-staking
- configMap:
defaultMode: 420
name: chain-aliases
name: chain-aliases
- configMap:
defaultMode: 420
name: luxd-chain-upgrades
name: chain-upgrades
updateStrategy:
type: OnDelete
volumeClaimTemplates:
- apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
app.kubernetes.io/instance: luxd
app.kubernetes.io/name: luxd
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
storageClassName: do-block-storage
volumeMode: Filesystem