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]>
302 lines
14 KiB
YAML
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.
|