Files
universe/deploy
zeekay 8a65db078a Lux mainnet and testnet are served from their archives on AWS
The canonical C-Chains are the RLP archives, 1,082,781 blocks on mainnet and
218 on testnet. Each runs as luxd-archive on the chains node and answers
publicly through the rpc door, which passes only eth_/net_/web3_ methods and
answers admin_, debug_, personal_ and txpool_ itself.

Testnet is frozen: its database is an emptyDir rebuilt from the archive
inside VM initialization on every boot, so its admin API is never on and no
restart depends on state surviving one. Mainnet is too large for that and
is imported once with admin_importChain; its admin API is behind the door,
and the pod is OnDelete and must not be deleted until luxfi/evm#152 is fixed
(after an import, a restart leaves the C-Chain unable to initialize).

Genesis is declared. The luxd-genesis ConfigMaps now hold the documents the
archives chain to: mainnet block 0 0x3f4fa2a0…, testnet 0x1c5fe377…,
each verified by booting it. The testnet document here before was a
different one (gasLimit 0x3b9aca00) that refuses every archived block, and
the mainnet one carried a C-Chain genesis that hashes to 0x43befc4e….

api.lux.network and api.lux.cloud route /v1/chain and / to the door at
priority 300, above the unified api-* router, and no longer point at
validators on the LAN.

The five-validator luxd sets are 0. They start from a genesis the archive
does not chain to and import nothing, so any block they made would fork
it. They come back on the canonical genesis once #152 is fixed.
2026-09-22 23:53:53 -07:00
..

deploy — the fleet, and the only thing that deploys it

Every service is one values file:

deploy/<namespace>/<name>.yaml

That is the whole inventory. Adding a service is adding a file; there is no second list to keep in step with it, and pin.sh --which reads the answer to "who runs this image" out of the directory rather than out of a table.

Each file is values for one shared chart, referenced by version:

oci://oci.hanzo.ai/charts/app:0.1.11
helm template <name> oci://oci.hanzo.ai/charts/app --version 0.1.11 \
  -n <namespace> -f deploy/<namespace>/<name>.yaml

What actually runs

The lux-fleet generator and its fence live in hanzo/universe infra/k8s/hanzo-cd/project-lux.yaml; they read this repo from GitHub and deploy to the cluster named lux-cluster. a git generator over deploy/*/*.yaml, one Application per file, destination lux-cluster, and a templatePatch that reads each file's own cd.automated:

syncPolicy:
  automated:
    prune: false
    selfHeal: true

19 of the 122 values files set cd.automated: true: the static sites, each verified answering from the edge on the current cluster. Every pod-based file is false until it is shown to run there; a pin bump does not opt a file back in. This file previously said the ApplicationSet "deliberately has no automated block" and that "there is no CD in this repository yet"; neither has been true since the templatePatch was added, and reading it that way is how a hand-edited object gets healed back and nobody knows why.

prune: false throughout. Nothing here deletes: a values file that stops declaring an object leaves that object running, which is why removing a service is a deliberate act on the cluster and not a git rm.

Moving an image — pin.sh

deploy/pin.sh <namespace>/<service> <version>   # a release
deploy/pin.sh --verify [<service>…]             # every pin, against its registry
deploy/pin.sh --which <repository>              # who runs this image
deploy/pin.sh --freeze <service>…               # a moving tag names bytes

cd.hanzo.ai does not watch the registry — it watches this directory. Publishing an image changes nothing; moving the pin here is the deploy. .hanzo/workflows/ image-published.yml is the only automated caller, and it does nothing else.

A pin is two fields and one fact. tag is how a human says which release; digest is what the kubelet pulls. The chart renders repo:tag@digest, and a reference carrying both resolves by digest — so a tag written from one registry read and a digest from another can name two different releases, and the pod then reports the tag while running the digest. pin.sh resolves both from one HEAD /v2/<path>/manifests/<tag> and writes them in one edit.

That is not hypothetical here. zoo/exchange-api pinned 2.3.34 to a digest that 2.3.34 no longer resolves to, beside three other deployments of the same version with no digest at all: four pods, one version number, two sets of bytes.

These files were generated from what is RUNNING

Not from the manifests in k8s/. Those were applied by hand over a long period and their currency is unknown. The running fleet is the only statement of what is true, so it is what was read.

Where the two disagreed, k8s/ was the stale one — and where k8s/ held something these files did not (an IngressRoute, a ConfigMap, a claim, a Role), that object moved here and the manifest was deleted. See k8s/README.md for the two objects that could not move and why.

selectorLabels — why most files have one

spec.selector cannot be changed after a workload is created. Most services here select on app: <name>, not the chart's app.kubernetes.io/name + instance, so each file records the selector its workload already carries. Without it the chart would emit its own and the apply would be refused.

It is the selector only — the full label set on metadata is still the chart's.

Verifying, before changing anything

# render every file and diff it against the live object
python3 <universe>/charts/app/hack/diff-live.py --context <ctx> --values deploy
  • BREAKING — the apiserver would refuse the apply. Only a workload's spec.selector qualifies; it is immutable.
  • DRIFT — live differs from the file. Someone changed one of them.
  • BENIGN — an intended consequence of moving to the chart.

What is NOT here

Chain CRs (LuxChain, LuxNetwork, LuxIndexer, LuxGateway, LuxMPC) are not services and have their own controllers. They stay in k8s/.

Secrets are not here and never will be. The chart has no way to carry a secret value: kmsSecrets names a path in KMS and the operator syncs it.

lux-system/s3 is deliberately absent. hack/live2values.py generated one from Deployment/s3 and both halves were wrong: that Deployment is a 0-replica husk, and Service/s3 selects app: seaweedfs with a single endpoint on a StatefulSet — so a sync would have repointed it at a selector matching zero pods and taken S3 down for every in-cluster consumer. Correcting it means picking one name for the thing, and renaming a StatefulSet is a recreate against its bound volume. Until that lands, lux-system S3 is visibly outside CD, which is the truthful state; a values file claiming otherwise is worse than an absence.

Why the chart is pinned at 0.1.11 and not 0.1.13

One values file does not render under 0.1.13:

deploy/lux-cloud/price.yaml
    kms: Additional property hostAPI is not allowed
    kms: Additional property credentialsSecret is not allowed

0.1.13 moved KMSSecrets to a ZAP transport: transport + zapAddr replace hostAPI and the per-CR credential, and the rendered object's group changes from secrets.lux.network/v1alpha1 to kms.hanzo.ai/v1. That is a cluster-side change, not a values edit — the lux KMS operator has to serve the new group, and the ApplicationSet's ignoreDifferences still names the old one. The chart's default zapAddr is cloud.hanzo.svc:9653, which is a hanzo-cluster address; Lux's KMS plane is kms.lux.cloud, and what its ZAP endpoint is has not been established.

So the bump is a migration with a cluster prerequisite, and it is not done here. The rest of 0.1.13 is already satisfied: image.digest is present wherever the chart's claim guard requires it, and lux-explorer/lux-faucet no longer carries its version inside image.repository. Under 0.1.11 all 122 files render.

Pins still to move

pin.sh --verify reads 121 pins and 116 agree with their registry. The five that do not are credentials rather than defects:

Pin Why it cannot be read
hanzo/lux-admin registry.digitalocean.com, no DO registry credential
lux-devnet/explore-fe-pars, lux-testnet/explore-fe-pars, lux-mainnet/explore-fe-pars ghcr.io/parsdao/explore answers 403 to a luxfi token

About a hundred services still carry a concrete version with no digest. Those are not ambiguous the way :latest was — the tag names a release — but a registry tag is mutable, which is how zoo/exchange-api came to be pinned at bytes its own version no longer meant. The end state is a digest beside every tag, written by the release that built it. pin.sh <service> <version> does that for any service whose repo dispatches on publish; --freeze is the interim for one that does not.