Files
zeekayandHanzo Dev 7417193940 A chain answers under one segment, and the fleet asks for it there
luxd moved chain APIs from /v1/bc/<alias> to /v1/chain/<alias> and deleted the
old prefix, and health moved onto typed ops at /v1/health/ops/*. Both landed in
node 1.36.181. Nothing here could move before that image existed, and the image
cannot roll out while the manifests still ask for addresses it no longer serves,
so the pin and the paths move in one commit.

What moved:
  image    1.36.171 -> 1.36.181, digest with it, on devnet/testnet/mainnet
  internal exchange-api, lux-hb2, rpc-tip-gate, the luxd startup script
  probes   /v1/health/liveness -> /v1/health/ops/liveness (bare /v1/health,
           which the local compose healthcheck used, no longer routes at all)
  public   the devnet and testnet gateway routes

Comments that RECORD an observation keep their original spelling: a note saying
/v1/bc/zoo/rpc returned 404 on a given date is a fact about that date, and
rewriting it would make the record false. Only live addresses moved.

1.36.181 also carries chains v1.7.38, so the deployed node has the zkvm
content-derived transaction identity.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-30 16:17:20 -07:00

302 lines
14 KiB
YAML

# luxd — devnet's five-validator CD-owned StatefulSet.
# OnDelete and Retain/Retain preserve quorum and ledger data during releases.
# Bootstrap remains enabled so a lagging validator catches up before voting.
nameOverride: luxd
containerName: luxd
replicas: 5
workload:
kind: StatefulSet
serviceName: luxd-headless
podManagementPolicy: Parallel
strategy: Recreate
image:
repository: ghcr.io/luxfi/node
# Semver and immutable digest move together.
tag: 1.36.181
digest: sha256:3ff4841e76b407e774b59ea33cdb691bc0b5b6dfdebc0e5ea71bd1119d059cad
pullPolicy: Always
ports:
- containerPort: 9632
name: zap-rpc
- containerPort: 9650
name: http
- containerPort: 9651
name: staking
- containerPort: 9090
name: metrics
service:
type: ClusterIP
# NOT headless. `luxd` is the ClusterIP clients dial; peer discovery uses the
# SEPARATE `luxd-headless` Service named by workload.serviceName above.
# headless:true rendered `luxd` itself with clusterIP None, and clusterIP is
# immutable, so every sync failed with
# Service "luxd" is invalid: spec.clusterIPs[0]: Invalid value: ["None"]
# ⚠️ luxd-headless is not rendered by this chart and is currently unmanaged
# (it was owned by the LuxNetwork CR that has been retired). It exists and
# works; bringing it under CD is the remaining gap here.
headless: false
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'
resources:
limits:
cpu: '6'
memory: 12Gi
requests:
cpu: '2'
memory: 4Gi
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
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
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
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:1.36.181@sha256:3ff4841e76b407e774b59ea33cdb691bc0b5b6dfdebc0e5ea71bd1119d059cad
imagePullPolicy: IfNotPresent
name: startup-gate
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
persistence:
enabled: false
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\n\
SRC_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\n\
mkdir -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'\n\
cp '/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 --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"
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- sleep 5
livenessProbe:
failureThreshold: 5
httpGet:
path: /v1/health/ops/liveness
port: 9650
scheme: HTTP
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 5
readinessProbe:
exec:
command:
- /bin/sh
- -c
- 'B=$(curl -sf -m 4 -X POST http://127.0.0.1:9650/v1/info -H ''Content-Type: application/json'' -d ''{"jsonrpc":"2.0","id":1,"method":"info.isBootstrapped","params":{"chain":"C"}}''
| grep -c ''"isBootstrapped":true''); [ "$B" = "1" ] || exit 1; 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_blockNumber","params":[]}'' | grep
-q ''"result":"0x'' || exit 1'
failureThreshold: 4
initialDelaySeconds: 30
periodSeconds: 15
successThreshold: 1
timeoutSeconds: 6
startupProbe:
failureThreshold: 60
httpGet:
path: /v1/health/ops/liveness
port: 9650
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
# ⚠️ The key is `app.kubernetes.io/name`, NOT `app`. These pods carry the
# chart's standard labels and have no bare `app` label at all, so the previous
# `key: app` selector matched ZERO pods — and an anti-affinity that selects
# nothing is indistinguishable from one that is satisfied. It was inert for as
# long as it has existed: lux-testnet ran THREE of five validators on
# lux-nodes-374ju1 (losing that one node drops below alpha=4 and halts the
# chain) while reporting a `required` constraint, and lux-mainnet's clean
# five-node spread was luck rather than enforcement.
# IgnoredDuringExecution means correcting this moves nothing now; it binds the
# next time a pod is scheduled.
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- luxd
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 150
imagePullSecrets:
- name: ghcr-luxfi
fsGroup: 1000
podAnnotations:
kubectl.kubernetes.io/restartedAt: '2026-07-26T23:11:09-07:00'
minReadySeconds: 10
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
podSecurityContext:
runAsGroup: 1000
runAsUser: 1000
component: validator
partOf: lux-network
topologySpread:
enabled: false
# luxd — devnet's five-validator CD-owned StatefulSet.
# OnDelete and Retain/Retain preserve quorum and ledger data during releases.
# Bootstrap remains enabled so a lagging validator catches up before voting.
# ⛔ Deliberately NOT using the chart's `pdb:` here. Its selector is
# selectorLabels (name+instance), which on this fleet ALSO matches the hourly
# luxd-export-c-chain job pods — 8 pods on lux-mainnet, not 5. A budget that
# counts finished Job pods as unhealthy drives disruptionsAllowed to 0 and
# blocks legitimate node drains, which is the failure this was meant to prevent.
# The validators are pinned by `app.kubernetes.io/component: validator`, a key
# the chart's PDB template cannot express, so `luxd-pdb` stays hand-applied
# until the chart grows an extra-selector option.
#
# The long-standing luxd-pdb was INERT: it demanded
# `app.kubernetes.io/managed-by: lux-operator`, a fossil from before this fleet
# moved to the chart (pods now say `Helm`), so it matched ZERO pods and reported
# expectedPods=0 for 162 days. Corrected 2026-08-06 to
# {name: luxd, component: validator} — verified selecting exactly 5.