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

247 lines
12 KiB
YAML

# luxd StatefulSet — lux-mainnet (5 validators, C-Chain EVM 96369).
#
# This is the object the kubelet actually reads. lux-mainnet declares the luxd
# image in THREE places and only this one moves a pod:
# 1. StatefulSet/luxd — this file. No ownerReferences, so nothing
# reconciles it; a plain `kubectl apply` is the only
# writer and git is the only durable source.
# 2. LuxNetwork lux.network/v1alpha1/luxd — network.yaml.
# 3. LuxNetwork lux.cloud/v1/luxd — live object, no file in this repo.
# Both operators (lux-operator, lux-operator-devnet) sit at 0/0 and MUST STAY
# THERE, so neither CR moves anything. All three read v1.36.2 today.
#
# Committed after the 2026-07-28 v1.36.35 roll attempt, which changed this
# object in the cluster and reverted it, with no file to record either edit —
# the sister manifest ../lux-testnet/luxd-statefulset.yaml had been tracked
# since 2026-07-26 and mainnet, the higher-stakes namespace, had not. Captured
# verbatim from the live object; `kubectl diff -f` is clean, checked against a
# mutated copy as the negative control.
#
# updateStrategy is OnDelete: changing spec.template here does NOT roll the
# fleet. Pods adopt a new image only when deleted individually, which is the
# intended safety property on mainnet — see network.yaml for the two blockers
# that currently make deleting ANY luxd pod in this namespace destructive.
#
# persistentVolumeClaimRetentionPolicy is Retain/Retain: deleting or scaling
# this StatefulSet never removes a data-luxd-N PVC.
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app.kubernetes.io/instance: luxd
app.kubernetes.io/managed-by: lux-operator
app.kubernetes.io/name: luxd
name: luxd
namespace: lux-mainnet
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
podManagementPolicy: Parallel
replicas: 5
revisionHistoryLimit: 10
selector:
matchLabels:
app.kubernetes.io/instance: luxd
app.kubernetes.io/managed-by: lux-operator
app.kubernetes.io/name: luxd
serviceName: luxd-headless
template:
metadata:
annotations:
forceRestart: '1780781328'
kubectl.kubernetes.io/restartedAt: '2026-06-06T14:28:28-07:00'
labels:
app: luxd
app.kubernetes.io/instance: luxd
app.kubernetes.io/managed-by: lux-operator
app.kubernetes.io/name: luxd
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- luxd
topologyKey: kubernetes.io/hostname
containers:
- command:
- /bin/sh
- /scripts/startup.sh
env:
- name: GOMEMLIMIT
value: 6GiB
- name: ZAP_RPC_LISTEN
value: :9632
- name: LUX_NETWORK_ID
value: '1'
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: LUX_CONSENSUS_VIEW_CHANGE
value: 'true'
image: ghcr.io/luxfi/node:v1.36.125@sha256:f65281b532a6b2294b83f57610cad2d292a12cf5c85232986bd0b54979d76862
imagePullPolicy: Always
livenessProbe:
failureThreshold: 10
httpGet:
path: /v1/health/liveness
port: 9630
scheme: HTTP
initialDelaySeconds: 300
periodSeconds: 15
successThreshold: 1
timeoutSeconds: 5
name: luxd
ports:
- containerPort: 9632
name: zap-rpc
protocol: TCP
- containerPort: 9630
name: http
protocol: TCP
- containerPort: 9631
name: staking
protocol: TCP
- containerPort: 9090
name: metrics
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /v1/health/liveness
port: 9630
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
resources:
limits:
cpu: '4'
memory: 12Gi
requests:
cpu: 100m
memory: 7Gi
startupProbe:
failureThreshold: 30
initialDelaySeconds: 5
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 9630
timeoutSeconds: 1
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /data
name: data
- mountPath: /scripts
name: startup-script
- mountPath: /staking-keys
name: staking-keys
readOnly: true
dnsPolicy: ClusterFirst
initContainers:
- args:
- "set -eu\n# \u2500\u2500 Lux MAINNET C-Chain RLP recovery init \u2500\u2500\n# NO CHAIN ID IS NAMED HERE. The previous version hardcoded\n# CBID=<a now-dead chain ID> and ran `rm -rf /data/chainData/$NET/$CBID`. That ID\n# stopped existing at the re-genesis (the live C-Chain is\n# 2T11SNJM9KYqSenoPb5yv6qahmj8HLefCjxt4gTD2iQWTHwdRu), so the wipe was a SILENT\n# NO-OP \u2014 the same drift that made `.wipe-cchain` useless and forced the\n# snapshot-clones that corrupted luxd-0 and luxd-2.\n#\n# There is now ONE wipe implementation, in the startup script, keyed off\n# `.wipe-state`. This container no longer wipes anything itself: it verifies the\n# artifact and drops the markers. An RLP import replays from genesis and rebuilds\n# every index, so discarding ALL chain state is both correct and simpler than\n# targeting one directory \u2014 and it cannot drift.\n#\n# Order: [arm-gate] -> [sentinel-skip] -> [SHA-verify] -> [markers].\n# SHA-verify MUST precede the wipe request; a mismatch aborts before anything\n# destructive is scheduled.\nDB=/data/db\nRLP=/data/lux-mainnet-96369.rlp\nEXPECTED_SHA=dd6b71f510fca48080c383bf44fccbfda4849e2cde06710ffe4a24fa1d9bec79\nH=dd6b71f510fc\nARM=\"$DB/.rlp-import-arm\"\nSENTINEL=\"$DB/.tenant-import-c-chain-$H\"\nMARKER=\"$DB/.tenant-import-request-c-chain-$H.json\"\nif [ ! -f \"$ARM\" ]; then\n echo \"[rlp-import-init] node NOT armed (no $ARM); skipping\"\n exit 0\nfi\nif [ -f \"$SENTINEL\" ]; then\n echo \"[rlp-import-init] sentinel present; already imported; clearing arm\"\n rm -f \"$ARM\"\n exit 0\nfi\nif [ ! -f \"$RLP\" ]; then\n echo \"[rlp-import-init] ERROR: RLP not found at $RLP\" >&2\n exit 1\nfi\nGOT=$(sha256sum \"$RLP\" | awk '{print $1}')\nif [ \"$GOT\" != \"$EXPECTED_SHA\" ]; then\n echo \"[rlp-import-init] SHA MISMATCH want=$EXPECTED_SHA got=$GOT -- ABORT, nothing wiped\" >&2\n exit 1\nfi\necho \"[rlp-import-init] SHA OK ($GOT)\"\n# Request the canonical full-state wipe. The startup script consumes .wipe-state\n# and clears /data/chainData + every /data/db DATABASE, while preserving\n# /data/staking (identity) and every FILE at /data/db root (this RLP included).\ntouch \"$DB/.wipe-state\"\necho \"[rlp-import-init] requested canonical wipe via .wipe-state (no chain ID named)\"\nprintf '{\"tenant\":\"c-chain\",\"chainAlias\":\"C\",\"contentHash\":\"%s\",\"rlpPath\":\"%s\",\"sentinelPath\":\"%s\"}' \"$H\" \"$RLP\" \"$SENTINEL\" > \"$MARKER.tmp\"\nmv \"$MARKER.tmp\" \"$MARKER\"\nrm -f \"$ARM\"\necho \"[rlp-import-init] marker=$MARKER; import driven on /v1/chain/C/rpc\"\n"
command:
- /bin/sh
- -c
env:
- name: LUX_CONSENSUS_VIEW_CHANGE
value: 'true'
image: ghcr.io/luxfi/node:v1.36.125@sha256:f65281b532a6b2294b83f57610cad2d292a12cf5c85232986bd0b54979d76862
imagePullPolicy: IfNotPresent
name: rlp-import
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /data
name: data
- args:
- "#!/bin/sh\nset -e\n\nPOD_INDEX=${HOSTNAME##*-}\necho \"[GATE] Startup gate for $HOSTNAME (luxd,\
\ network-id=1)\"\nDNS_PREFIX=\"luxd\"\nDNS_SUFFIX=\"luxd-headless.lux-mainnet.svc.cluster.local\"\
\nGATE_MIN_PEERS=2\nGATE_TIMEOUT=60\nGATE_INTERVAL=5\nGATE_START=$(date +%s)\necho \"[GATE]\
\ Waiting for $GATE_MIN_PEERS peers (timeout ${GATE_TIMEOUT}s)...\"\n\nwhile true; do\n REACHABLE=0\n\
\ if [ \"$POD_INDEX\" != \"0\" ]; then\n if nc -z -w 2 \"${DNS_PREFIX}-0.${DNS_SUFFIX}\"\
\ 9631 2>/dev/null; then\n REACHABLE=$((REACHABLE + 1))\n fi\n fi\n if [ \"$POD_INDEX\"\
\ != \"1\" ]; then\n if nc -z -w 2 \"${DNS_PREFIX}-1.${DNS_SUFFIX}\" 9631 2>/dev/null; then\n\
\ REACHABLE=$((REACHABLE + 1))\n fi\n fi\n if [ \"$POD_INDEX\" != \"2\" ]; then\n\
\ if nc -z -w 2 \"${DNS_PREFIX}-2.${DNS_SUFFIX}\" 9631 2>/dev/null; then\n REACHABLE=$((REACHABLE\
\ + 1))\n fi\n fi\n if [ \"$POD_INDEX\" != \"3\" ]; then\n if nc -z -w 2 \"${DNS_PREFIX}-3.${DNS_SUFFIX}\"\
\ 9631 2>/dev/null; then\n REACHABLE=$((REACHABLE + 1))\n fi\n fi\n if [ \"$POD_INDEX\"\
\ != \"4\" ]; then\n if nc -z -w 2 \"${DNS_PREFIX}-4.${DNS_SUFFIX}\" 9631 2>/dev/null; then\n\
\ REACHABLE=$((REACHABLE + 1))\n fi\n fi\n\n if [ \"$REACHABLE\" -ge \"$GATE_MIN_PEERS\"\
\ ]; then\n echo \"[GATE] $REACHABLE peers reachable, proceeding with startup\"\n break\n\
\ fi\n\n ELAPSED=$(( $(date +%s) - GATE_START ))\n if [ \"$ELAPSED\" -ge \"$GATE_TIMEOUT\"\
\ ]; then\n echo \"[GATE] WARNING: Timeout after ${ELAPSED}s with only $REACHABLE peers.\
\ Proceeding (onTimeout=StartAnyway).\"\n break\n fi\n\n echo \"[GATE] $REACHABLE/$GATE_MIN_PEERS\
\ peers reachable (${ELAPSED}/${GATE_TIMEOUT}s), waiting ${GATE_INTERVAL}s...\"\n sleep \"\
$GATE_INTERVAL\"\ndone\n\necho \"[GATE] Gate passed, luxd may start.\"\n"
command:
- /bin/sh
- -c
env:
- name: LUX_CONSENSUS_VIEW_CHANGE
value: 'true'
image: ghcr.io/luxfi/node:v1.36.125@sha256:f65281b532a6b2294b83f57610cad2d292a12cf5c85232986bd0b54979d76862
imagePullPolicy: IfNotPresent
name: startup-gate
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
priorityClassName: luxd-validator-critical
restartPolicy: Always
schedulerName: default-scheduler
securityContext:
fsGroup: 1000
runAsGroup: 1000
runAsUser: 1000
serviceAccount: default
serviceAccountName: default
terminationGracePeriodSeconds: 150
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
volumes:
- configMap:
defaultMode: 420
name: luxd-config
name: config
- configMap:
defaultMode: 493
name: luxd-startup
name: startup-script
- name: staking-keys
secret:
defaultMode: 420
secretName: luxd-staking
updateStrategy:
type: OnDelete
volumeClaimTemplates:
- apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 200Gi
volumeMode: Filesystem
status:
phase: Pending