262 lines
15 KiB
YAML
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
|