Files
zeekayandHanzo Dev b1059c5e6f Lux's chain archives are declared for CD
deploy/lux-{mainnet,testnet}/luxd-archive.yaml are charts/app values for
the two luxd archives that serve api.lux.network, api.lux.cloud and
api.lux-test.network. They replace the hand-applied k8s/ manifests and
deploy/lux-api/route.yaml, which declared the same objects.

Rendered and dry-run against the running objects (server-side, the way
CD applies), a sync adds the chart's labels, its config checksum and
HANZO_VERSION, and nothing else: same container, genesis, chain config,
volumes and routes. So cd.automated is off until someone syncs one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 19:31:29 -07:00

273 lines
11 KiB
YAML

# luxd — testnet's five-validator CD-owned StatefulSet.
# OnDelete and Retain/Retain preserve quorum and ledger data during releases.
nameOverride: luxd
containerName: luxd
# 0, not 5. The canonical lux-testnet C-Chain is the RLP archive, served by
# deploy/lux-testnet/luxd-archive.yaml. These validators start from a genesis the
# archive does not chain to and import nothing, so any block they make is a
# fork of it. They return once luxfi/evm#152 is fixed (a validator that
# imports and then restarts cannot initialize its C-Chain) and they run the
# canonical genesis with the archive imported.
replicas: 0
workload:
kind: StatefulSet
serviceName: luxd-headless
podManagementPolicy: Parallel
strategy: Recreate
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
volumeClaimTemplates:
- apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 200Gi
volumeMode: Filesystem
persistence:
enabled: false
service:
type: ClusterIP
headless: false
image:
# Semver and immutable digest move together.
repository: ghcr.io/luxfi/node
tag: 1.36.181
pullPolicy: Always
digest: sha256:3ff4841e76b407e774b59ea33cdb691bc0b5b6dfdebc0e5ea71bd1119d059cad
command:
- /bin/sh
- /scripts/startup.sh
env:
# GOMEMLIMIT is the half that actually prevents an OOM kill. It is a SOFT limit:
# the Go runtime GCs harder as it approaches, instead of sailing into the cgroup
# ceiling and being SIGKILLed. lux-mainnet has always set it (6GiB under a 12Gi
# limit) and does not OOM; lux-testnet carried NO env at all, and luxd-0 and
# luxd-4 died OOMKilled (exit 137) 160 and 149 times respectively.
#
# The trigger is being BEHIND, not being busy: luxd-1/luxd-2, caught up, sit at
# ~760-800Mi, while a node catching up (and serving ancestry to other catching-up
# peers) holds several times that. So the two nodes that most needed to finish
# syncing were the two the ceiling killed first — and each kill restarted the
# sync from a cold cache, which is why neither ever converged.
#
# Held below the hard limit on purpose. The LIMIT is what kills, so only the
# limit is raised; `requests` stays at 1Gi because it is the scheduler's
# RESERVATION and raising it can make a pod permanently Pending on a committed
# node pool without changing the OOM behaviour at all.
- name: GOMEMLIMIT
value: 6GiB
- name: ZAP_RPC_LISTEN
value: :9632
- name: LUX_NETWORK_ID
value: '2'
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: LUX_CONSENSUS_VIEW_CHANGE
value: 'true'
- name: VIEW_CHANGE
value: 'true'
ports:
- containerPort: 9632
name: zap-rpc
protocol: TCP
- containerPort: 9640
name: http
protocol: TCP
- containerPort: 9641
name: staking
protocol: TCP
- containerPort: 9090
name: metrics
protocol: TCP
resources:
limits:
cpu: '2'
memory: 8Gi
requests:
cpu: 500m
memory: 1Gi
volumeMounts:
- mountPath: /data
name: data
- mountPath: /scripts
name: startup-script
- mountPath: /staking-keys
name: staking-keys
readOnly: true
livenessProbe:
failureThreshold: 10
httpGet:
path: /v1/health/ops/liveness
port: 9640
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 15
successThreshold: 1
timeoutSeconds: 5
readinessProbe:
exec:
command:
- /bin/sh
- -c
- 'B=$(curl -sf -m 4 -X POST http://127.0.0.1:9640/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:9640/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: 30
initialDelaySeconds: 5
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 9640
timeoutSeconds: 1
volumes:
- name: data
persistentVolumeClaim:
claimName: data-luxd-1
- 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
initContainers:
- args:
- "set -eu\n# \u2500\u2500 Lux C-Chain RLP recovery init (incident-1082814) \u2500\u2500\n# Order: [arm-gate]\
\ -> [sentinel-skip] -> [SHA-verify] -> [WIPE] -> [marker].\n# SHA-verify MUST precede the wipe; mismatch\
\ aborts before any destructive op.\nDB=/data/db\nNET=network-2\nCBID=2dbTRWZrj65UZQe6yWruV1jEiq9oR9hawVF8SRwm9UtpGder6p\n\
RLP=/data/lux-testnet-96368.rlp\nEXPECTED_SHA=2b9667bd68994533139b3a16e85e418df30967e7961bd32979fab770c52fd2d4\n\
H=2b9667bd6899\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 (no wipe)\"\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 before wipe\"\
\ >&2\n exit 1\nfi\necho \"[rlp-import-init] SHA OK ($GOT) -- proceeding to wipe C-Chain\"\nTARGET=\"\
/data/chainData/$NET/$CBID\"\necho \"[rlp-import-init] wiping $TARGET\"\nrm -rf \"$TARGET\"\nprintf\
\ '{\"tenant\":\"c-chain\",\"chainAlias\":\"C\",\"contentHash\":\"%s\",\"rlpPath\":\"%s\",\"sentinelPath\"\
:\"%s\",\"blockchainID\":\"%s\"}' \"$H\" \"$RLP\" \"$SENTINEL\" \"$CBID\" > \"$MARKER.tmp\"\nmv \"\
$MARKER.tmp\" \"$MARKER\"\nrm -f \"$ARM\"\necho \"[rlp-import-init] wipe done; marker=$MARKER; startup.sh\
\ will admin_importChain on /v1/chain/C/rpc\"\n"
command:
- /bin/sh
- -c
env:
- name: LUX_CONSENSUS_VIEW_CHANGE
value: 'false'
- name: VIEW_CHANGE
value: 'false'
image: ghcr.io/luxfi/node:1.36.181@sha256:3ff4841e76b407e774b59ea33cdb691bc0b5b6dfdebc0e5ea71bd1119d059cad
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=2)\"\
\nDNS_PREFIX=\"luxd\"\nDNS_SUFFIX=\"luxd-headless.lux-testnet.svc.cluster.local\"\nGATE_MIN_PEERS=2\n\
GATE_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}\" 9641 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}\" 9641 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}\"\
\ 9641 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}\" 9641 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}\" 9641 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: 'false'
- name: 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
# ⚠️ 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: 120
fsGroup: 1000
podSecurityContext:
runAsGroup: 1000
runAsUser: 1000
podAnnotations: {}
topologySpread:
enabled: false
partOf: lux
component: validator
# luxd — testnet's five-validator CD-owned StatefulSet.
# OnDelete and Retain/Retain preserve quorum and ledger data during releases.
# ⛔ 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.