Asked to register both with CreateChainTx on Lux mainnet. Neither was created, and the record now says why rather than leaving the next reader to rediscover it. Each brand already holds a chain identity elsewhere: hanzod serves 36963 on its own primary network, bootstrapped, and Zoo's C-chain carries 13,348 blocks on network 200200 since the 2026-07-14 cutover. A CreateChainTx here would mint a second identity for the same brand and orphan that history. It also could not have run: platformvm now serves one handler at /ops and nothing at the chain base, so the JSON-RPC client both tools open with returns 404 before it reads anything, and every genesis allocation that could pay the fee is either staked or behind a mnemonic that is not on this machine. Records the eleven blockchain ids the primary network does carry, read from the P-chain rather than from a manifest. T is gone; F and M are there in its place, which the 2026-06-02 inventory predates. Marks that inventory and the brand-L2 follow-on it promised as superseded, since both still read as the plan. Co-authored-by: Hanzo Dev <[email protected]>
64 KiB
LLM.md
This file provides guidance to AI tools when working with code in this repository.
Project Overview
This is a multi-blockchain infrastructure project managing 4 Lux app chain EVM networks (LUX, ZOO, SPC, and Hanzo). The infrastructure includes blockchain nodes, block explorers, DeFi exchange, Graph Protocol indexers, cross-chain bridges, and IPFS storage.
Deterministic validator deployment
The live luxd StatefulSets MUST equal the three CD values files under
deploy/lux-{mainnet,testnet,devnet}/luxd.yaml. Those files are the only node
deployment source. Do not add luxd workloads under k8s/, hand-edit a live
StatefulSet, or restore a LuxNetwork operator as an alternate reconciliation
path.
The exact release and quorum-preserving rollout procedure is RUNBOOK.md and
its sole entry point is scripts/roll_luxd.sh. Every fleet has five validators
with a four-vote quorum. Roll devnet, then testnet, then mainnet; within a fleet,
replace only one validator at a time and require Ready + bootstrapped + exact
version + exact digest before continuing. Mainnet storage is never wiped.
Infrastructure: ZAP-native forks only — NEVER vanilla OSS
All data/storage/messaging backends MUST be the Hanzo ZAP-native forks, never upstream OSS
(they speak the native ZAP protocol — note the extra zap port on each):
| Need | USE (Hanzo fork) | NOT |
|---|---|---|
| analytics DB | Datastore ghcr.io/hanzoai/datastore (svc insights-datastore; secret datastore/DATASTORE_PASSWORD; data /var/lib/hanzo-datastore) |
ClickHouse |
| object store | hanzoai/s3 (lux-system s3 deploy, S3_ROOT_USER=hanzo/hanzo-s3-secret; aliases lux-assets/lux-cdn) |
MinIO |
| KV / cache | hanzoai/kv (kv/kv-0) |
Redis |
| SQL | hanzoai/sql (sql/sql-0) |
vanilla Postgres |
| KMS | hanzoai/kms (zapdb) | Infisical/Vault |
| IAM | hanzoai/iam (Casdoor fork) | — |
| pub/sub | hanzoai/pubsub (nats) |
Kafka |
Deploy by replicating the canonical fork workload from a working cluster (e.g. copy the
datastore StatefulSet + datastore-config ConfigMap + datastore Secret from hanzo-k8s).
Do NOT kubectl run clickhouse/minio or pull clickhouse/clickhouse-server etc. Refer to
them by the Hanzo name (Datastore, not ClickHouse).
Architecture
Network Configuration
CRITICAL: Network ID != C-Chain ID. Network IDs are well-known (1/2/3), C-Chain IDs are EVM chain IDs.
| Network | Network ID | C-Chain ID | Start Time |
|---|---|---|---|
| Mainnet | 1 | 96369 | Dec 25, 2025 4:20 PM PDT (1766704800) |
| Testnet | 2 | 96368 | Dec 25, 2025 4:20 PM PDT (1766704800) |
| Devnet | 3 | 96367 | Dec 25, 2025 4:20 PM PDT (1766704800) |
app chain EVM Chain IDs -- source of truth: NETWORKS.yaml
| app chain EVM | Coin | Mainnet | Testnet | Devnet |
|---|---|---|---|---|
| Zoo | ZOO | 200200 | 200201 | 200202 |
| Hanzo | AI | 36963 | 36962 | 36964 |
| SPC | SPC | 36911 | 36910 | 36912 |
| Pars | PARS | 494949 | 494950 | 494951 |
Note: Hanzo coin is AI, not HANZO. Hanzo testnet EVM chain ID is 36962; devnet is 36964.
Service Ports
| Service | Mainnet | Testnet | Description |
|---|---|---|---|
| Node 1-5 HTTP | 9630-9638 | 9740-9748 | RPC endpoints |
| Node 1-5 Staking | 9631-9639 | 9741-9749 | P2P staking |
| gRPC | 8369 | 8368 | gRPC API |
| LUX Explorer | 4010 | 4020 | Lux Explorer (Go binary) |
| Zoo Explorer | 4011 | 4021 | Lux Explorer (Go binary) |
| Hanzo Explorer | 4012 | 4022 | Lux Explorer (Go binary) |
| SPC Explorer | 4013 | 4023 | Lux Explorer (Go binary) |
| LXD Gateway | 8080 | 8081 | DEX API |
| PostgreSQL | 15432 | 15433 | Database |
| Redis | 16379 | 16380 | Cache |
Directory Structure
/home/z/work/lux
├── node/ # Lux Network node
├── geth/ # C-Chain implementation (formerly coreth)
├── evm/ # App chain EVM implementation
├── cli/ # Lux CLI for managing networks
├── vms/ # Virtual machine implementations
│ └── cchainvm/ # C-Chain VM utilities
├── exchange/ # Uniswap V2/V3 DEX (lux.exchange)
├── explorer-*/ # Blockchain explorers per network
├── monitoring/ # Health check and monitoring system
├── genesis-*/ # Genesis configurations per network
└── scripts/ # Management scripts
Recent Updates (2025)
Port Standardization
- All services now use port 9630 (previously 9650)
- Staking port: 9631
- Consistent across all configurations
Fixed Issues
- cchainvm Package: Created missing implementation at
/vms/cchainvm/slurp.go - Error Handling: Exported error constants in ids package (ErrNoAliasForID, ErrAliasAlreadyMapped)
- Hanzo Network: Deployment script ready at
/deploy-hanzo-network.sh - Database Connections: Fixed all explorer PostgreSQL connections
- Health Monitoring: Comprehensive system at
/monitoring/health_check_system.sh - Q-Chain Integration: Added quantum-resistant chain configuration with Crystal-Dilithium signatures
- admin_importChain Deadlock Fix (2026-01-05): Fixed P2P degradation after block import
Critical Fix: admin_importChain Deadlock (2026-01-05)
Problem: After admin_importChain, P-chain and Info APIs timeout while C-chain continues working.
Root Cause: PostImportCallback was called synchronously while holding chainmu.Lock(), causing cross-chain mutex contention with P-chain/Info API handlers.
Solution (coreth/eth/api_admin.go:180-196):
// CRITICAL: Run asynchronously to avoid deadlock
go func() {
if err := api.eth.CallPostImportCallback(lastInsertedBlock.Hash(), lastInsertedBlock.NumberU64()); err != nil {
log.Error("PostImportCallback failed", "error", err)
return
}
log.Info("ImportChain: post-import callback completed asynchronously")
}()
Build after fix:
cd ~/work/lux/node && CGO_ENABLED=0 ./scripts/build.sh
cd ~/work/lux/coreth && CGO_ENABLED=0 ./scripts/build.sh
Key Technologies
- Blockchain: Go (node, geth, evm), Lux-based
- Explorers: Go (Lux Explorer single binary: indexer+GraphQL+SPA)
- DeFi: TypeScript/React (Uniswap fork)
- Indexing: Rust (Graph Protocol)
- Infrastructure: Docker, Docker Compose, PostgreSQL
Gateway Security Hardening (2026-03-24)
Five security fixes applied across all gateway configs:
- info.peers/getNodeIP/getNodeID blocked -- CEL validation on all
/v1/info,/mainnet/v1/info,/testnet/v1/info,/devnet/v1/infoendpoints across all config layers (gateway.json, gateway-mainnet.json, testnet/devnet JSONs, K8s ConfigMap). K8s gateway.yaml was missing CEL on/mainnet/v1/info,/testnet/v1/info,/devnet/v1/info-- now fixed. - Response headers stripped --
modifier/response-headers-blacklistblocksX-Gateway,X-Gateway-Completed,node-id,Node-Id,Serverin all configs. Was already correct. - Per-endpoint rate limiting -- Every endpoint now has
qos/ratelimit/router: 100 req/s for RPC, 50 for WS, 30 for health, 10 for info. K8s gateway.yaml previously had zero per-endpoint limits. - CORS wildcard removed -- IngressRoute
cors-allow-allmiddleware replaced withgateway-cors(regex-based origin allowlist:*.lux.network,*.lux.exchange,lux.market). LuxGateway CRDs also updated from["*"]to explicit origin list. - XVM (exchangevm) routes added --
/v1/chain/exchangealias routes map to/v1/chain/Xon all networks (mainnet/testnet/devnet) in both K8s gateway and standalone configs. Follows same pattern as/v1/chain/dex/rpc->/v1/chain/D/rpc.
Files changed: configs/gateway/gateway.json, configs/gateway/gateway-mainnet.json, configs/gateway/gateway-testnet.json, configs/gateway/gateway-devnet.json, k8s/lux-gateway/gateway.yaml, k8s/lux-k8s/validators/crd-resources.yaml.
lux.exchange TLS: app.lux.exchange added to Ingress (2026-07-14)
Symptom: https://app.lux.exchange → ERR_SSL_PROTOCOL_ERROR (SNI returned no cert).
Root cause: k8s/lux-ns/lux-exchange.yaml Ingress omitted app.lux.exchange from both spec.tls[0].hosts and spec.rules, so hanzoai/ingress (Traefik) had no router/cert for that SNI. The lux-exchange-tls cert-manager secret already covers app.lux.exchange in its SAN (Ready), and DNS app.lux.exchange (Cloudflare grey-cloud, DNS-only) points at the LB 134.199.138.27.
Fix: Added app.lux.exchange to the Ingress tls.hosts + a rules entry → lux-exchange:3000. Traefik now serves the valid LE cert (verified in-cluster: CN=lux.exchange, SAN incl. app.lux.exchange, valid → 2026-09-29). DEX confirmed rendering via the unfiltered exchange.lux.network alias (same backend/cert). Cloudflare lux.exchange zone is healthy: active, SSL full, Universal cert covers lux.exchange + *.lux.exchange; apex/www proxied, app/trade grey-cloud.
Residual (NOT origin/ingress/cert): the lux.exchange domain is intercepted by safebrowse-style SNI/DNS egress filters on some client networks (crypto-exchange reputation lists) — proven: to the same origin IP, SNI zoo.exchange/bridge.lux.network handshake OK while SNI *.lux.exchange returns non-TLS bytes (wrong version number). Needs a domain-reputation/delisting request; not a k8s/Cloudflare fault. See header note in k8s/lux-ns/lux-exchange.yaml.
api.lux.cloud accepted no API key at all
Symptom: every sk-/pk- presented to api.lux.cloud answered 403 X-Org-Id required, which reads as "bad key". A JWT worked, so only machine credentials were dead — CI could never deploy.
Three independent causes, all in k8s/lux-k8s/cloud/deployment.yaml:
- No IAM client.
IAM_MINT_CLIENT_ID/IAM_MINT_CLIENT_SECRETwere unset, so cloud'siamCred()returned""and the key resolver short-circuited to nil before any network call. Now set from Secretcloud-iam-client(clienthanzo-cloud— the binary's own service identity, and one of the two IAM admits atget-user?accessKeyviaIAM_KEY_RESOLVE_APPS).lux-cloudcannot be used: it is a SPA and holds no secret. - Image too old.
v1.801.210predates "a machine credential resolves its org from the subject" (v1.801.245). Before that ansk-resolved to a principal with noorgsclaim, and the identity boundary — right to refuse a human token that lost its claim — refused the key too. Pinnedv1.801.476: the newest tag that is both above that floor and belowv1.801.487, where the console became a published site and boot began requiring a releasedlux/hanzo-consolethis front door does not have (v1.801.496crash-looped on exactly that). Tags abovev1.801.496have no image at all — a pin must be checked against ghcr, nevergit tag. - Probes pointed at a port the image does not open. Liveness/readiness hit
:9090/healthz+/readyz; this binary serves/healthzon the product listener only and readsCLOUD_HEALTH_LISTENnowhere, so liveness killed a process that was serving traffic. Probes now usehttp(8000)/healthz, and the phantomopsport is gone.
Also: the cluster ghcr-secret held a dead credential (403 Forbidden from ghcr's token endpoint). Nothing noticed because imagePullPolicy: IfNotPresent served every pod from node cache — until a pod landed on a node without the layer. Replaced with the working credential.
Store moved with the version: v1.801.476 keeps projects at /var/lib/cloud/orgs/_platform/projects.db; the pre-bump rows in /var/lib/cloud/projects.db are invisible to it, so the tel project was recreated (new id proj_4vW2NGyLKpHImFgVpg9Msg, org lux). Nothing had ever deployed to the old row.
Still owed on this host (not changed here — both are decisions, not defects): CLOUD_HOSTING_FEE_CENTS=0 (hanzo prod sets it; without it every deploy hits a billing gate this deployment has no commerce backend to answer, so 503 balance_unavailable), and S3_ADMIN_ACCESS_KEY/S3_ADMIN_SECRET_KEY + a reachable endpoint for CLOUD_PROJECTS_BUCKET (hanzo's S3 is in-cluster on hanzo-k8s and unreachable from lux-k8s).
Development Guidelines
Important Rules
- ALWAYS use luxfi packages, never use go-ethereum or upstream legacy packages
- Use
luxfi/gethfor Ethereum compatibility, NOT go-ethereum - Use
luxfi/idsNOTluxfi/node/ids(regression) - NEVER commit local replace directives in go.mod
- Always use semantic versioned tags, never make up git tags
- Port 9630 for luxd, NOT 9650
Running Networks
Option 1: Docker Compose (Recommended for Production)
cd ~/work/lux/universe
# Build and start mainnet (5 validators)
docker compose -f compose.mainnet.yml up -d --build
# Start testnet (5 validators)
docker compose -f compose.testnet.yml up -d --build
# Add block explorers
docker compose -f compose.mainnet.yml --profile explorers up -d
# View logs
docker compose -f compose.mainnet.yml logs -f mainnet-node-1
# Stop
docker compose -f compose.mainnet.yml down
Option 2: Lux CLI (Development)
# Build binaries with deadlock fix
cd ~/work/lux/node && CGO_ENABLED=0 ./scripts/build.sh
cd ~/work/lux/coreth && CGO_ENABLED=0 ./scripts/build.sh
cp ~/work/lux/node/build/luxd ~/.luxd/
# Start networks
lux network start --mainnet
lux network start --testnet
# Check status
lux network status
# Stop
lux network stop --mainnet --force
lux network stop --testnet --force
Block Import (After Network is Running)
# Write genesis state spec first
curl -s -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"admin_writeGenesisStateSpec","params":["/Users/z/work/lux/genesis/configs/mainnet/cchain.json"],"id":1}' \
http://127.0.0.1:9630/v1/chain/C/rpc
# Import blocks (mainnet C-Chain: 1,082,780 blocks)
curl -s -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"admin_importChain","params":["/Users/z/work/lux/state/rlp/lux-mainnet/lux-mainnet-96369.rlp"],"id":1}' \
http://127.0.0.1:9630/v1/chain/C/rpc
# Verify import
curl -s -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
http://127.0.0.1:9630/v1/chain/C/rpc
RLP Block Data Available
| Chain | File | Blocks |
|---|---|---|
| C-Chain Mainnet | state/rlp/lux-mainnet/lux-mainnet-96369.rlp |
1,082,780 |
| C-Chain Testnet | state/rlp/lux-testnet/lux-testnet-96368.rlp |
218 |
| Zoo Mainnet | state/rlp/zoo-mainnet/zoo-mainnet-200200.rlp |
799 |
| Zoo Testnet | state/rlp/zoo-testnet/zoo-testnet-200201.rlp |
84 |
| SPC Mainnet | state/rlp/spc-mainnet/spc-mainnet-36911.rlp |
11 |
POA Implementation
The node implements Proof of Authority consensus with:
- Modified Wave parameters (K=1, Alpha=1, Beta=1)
- Single-node blockchain operation
- No validator staking requirements
- Immediate block finalization
- Automining enabled for development
Common Tasks
Service Management
# Start monitoring daemon
./monitoring/health_check_system.sh --daemon
# Fix all RPC endpoints
./fix_all_rpc_endpoints.sh
# Deploy new app chain EVM
./deploy-hanzo-network.sh
# Start Q-Chain network
./launch-qchain.sh start
Database Operations
# Access database
psql -h localhost -p 5432 -U postgres -d explorer_lux # legacy DB; new Lux Explorer is DB-free
# Fix explorer connections
./fix_explorer_databases.sh
C-Chain Migration
# Import ancient blockchain data
./cmd/import-ancient/import-ancient \
-source /old/db \
-target /new/db \
-start 0 \
-end 1000000
API Endpoints
RPC Endpoints (Port 9630)
- Info API: http://localhost:9630/v1/info
- C-Chain RPC: http://localhost:9630/v1/chain/C/rpc
- C-Chain WebSocket: ws://localhost:9630/v1/chain/C/ws
- Health API: http://localhost:9630/v1/health
- Metrics: http://localhost:9630/v1/metrics
Public RPC Endpoints (via Gateway)
A network is a HOST, not a path prefix. There is no /mainnet/ or /testnet/
segment — measured 2026-09-01, every such form returns 404. The path is
/v1/<segment>/<chain>/rpc on that network's own host.
| Network | Host | C-Chain RPC |
|---|---|---|
| Lux mainnet | api.lux.network |
https://api.lux.network/v1/chain/C/rpc |
| Lux testnet | api.lux-test.network |
https://api.lux-test.network/v1/chain/C/rpc |
| Zoo | api.zoo.network |
https://api.zoo.network/v1/chain/C/rpc |
| Hanzo | api.hanzo.network |
https://api.hanzo.network/v1/chain/C/rpc |
A brand chain on the Lux host is addressed by its alias or blockchain ID in the
same position: https://api.lux.network/v1/chain/zoo/rpc.
The segment is chain, and that is now what answers. This paragraph used to
say the opposite — that the fleet still answered the older bc spelling and
that /v1/chain/C/rpc returned 404 — and it has been the wrong way round since
deploy/lux-api/route.yaml landed. Measured 2026-09-09:
POST https://api.lux.network/v1/chain/C/rpc eth_chainId -> 0x17871 200
POST https://api.lux.cloud/v1/chain/C/rpc eth_chainId -> 0x17871 200
POST https://api.lux.network/v1/bc/C/rpc 404
POST https://api.lux.network/ eth_chainId -> 0x17871 200
The bare host IS the C-chain, on both api.lux.network and api.lux.cloud: a
wallet is handed a host, so that host answers RPC at /. .../rpc is not a
third spelling of it and returns 404.
The trading surface, on the same two hosts
ghcr.io/luxfi/dex's gateway binary runs as lux-mainnet/trade and answers
everything under /v1/trade on api.lux.cloud and api.lux.network — eight
paths: venues, quote, quotes, swap, and the four allowance ones. Its
declaration is deploy/lux-mainnet/trade.yaml; api/openapi.yaml in luxfi/dex
is the contract, and a test in that repo holds the document to the route table
so neither can drift from the other.
The other half of the surface is the indexer, and that is not an accident.
Enumerating a chain — its tokens, pools, positions, TVL, price history — is an
index read, and api-explore.lux.cloud answers it: measured, 16 pools and
$39.9M of TVL for 96369 with real symbols and decimals. Those paths were on the
gateway too and answered 500 on every request, because a quoting surface reads
ONE pool by address and cannot walk every pool on Ethereum. A screen wanting a
market list asks the indexer; a screen wanting a price for a size asks the
gateway.
A market on 96369 is quoted from our own contracts and a market anywhere else from the pools that chain already holds — read over its own JSON-RPC, no vendor API and no upstream account. The shape is the same either way, so a screen never learns which arm answered:
POST /v1/trade/quote {"chainId":96369,"tokenIn":WLUX,"tokenOut":LUSD,…}
-> 997001483075056712 providerName uniswap_v3
POST /v1/trade/quote {"chainId":1,"tokenIn":WETH,"tokenOut":USDC,…}
-> 2512401632 providerName uniswap_v3
GET /v1/trade/venues is what a screen asks first — which chains this
deployment can price, and from what. 96369 comes back native with the
precompile arm beside the V3 quoter; the other six carry V2 and V3.
POST /v1/trade/swap is built by the venue that gave the price, against the
SwapRouter02 beside the quoter that answered — LUX_V3_ROUTER in the values
file for 96369, and Uniswap's own published router on every other chain. It
used to encode every quote for the PoolManager precompile at 0x9010, which
answers 0x on this chain. Proved by replaying the gateway's own calldata
through eth_call on the live router from the account holding the allowance:
POST /v1/trade/swap 1 WLUX -> LUSD -> to 0xbbD8d9…, fee 3000
eth_call that calldata -> 997001483075056712
the quote said 997001483075056712
Permit2 is not on 96369. 0x000000000022D473030F116dDEE9F6B43aC78BA3 holds
9152 bytes on Ethereum and nothing here, so both permit2 paths answer 404 on
this chain naming the reason rather than calldata addressed to an empty
account. Approve the spender directly; /v1/trade/approval/* does that and
reads the allowance from the chain.
Two routers on one host, and the chain's is the more specific. /v1/chain
keeps priority 300 and the root 310; trade takes 200, which is above the
bare-host router that answers these hosts out of hanzoai/cloud — api-hanzo in
the ingress ingress-dynamic ConfigMap, no priority stated, so Traefik gives it
its rule length, about 118. A cloud 404 on one of these hosts is recognisable by
the x-api-version: header it carries.
⚠ Today the route names BOTH shapes, and that is deliberate. The pin is
still the image that answers the older spellings, so a rule naming /v1/trade
alone made every quote a hanzoai/cloud 404 — a surface reachable, running, and
addressed by a name it does not answer to. A union is safe in both directions.
strip-role stays for the same reason: the pinned image still authorises
/v1/admin/* on a header the caller writes, and no rule here should be the only
thing between that and the internet. Both come out in the commit that moves the
pin past that version.
One prefix, where there used to be a nine-clause allowlist. The allowlist
existed for a real reason: the binary published /v1/admin/pool/freeze, so
PathPrefix(/v1) would have put an irreversible switch on the internet. Those
handlers are deleted and the whole surface moved under /v1/trade, so the
allowlist and the thing it guarded against went away together. /v1/leads and
/v1/events keep their old paths and stay reachable from inside the cluster and
nowhere else — not because the route omits them, but because they are not under
this prefix.
⚠ The file provider IS enabled on this ingress and its routers are live —
--providers.file.directory=/etc/traefik/dynamic, mounted from the
ingress-dynamic ConfigMap in the hanzo namespace, alongside
kubernetesingress and kubernetescrd. A router that seems to come from nowhere is
in that ConfigMap.
Production Infrastructure (March 2026)
Deployment Model
Each component deploys independently via semantic version tags and container images (GHCR). No submodules. Update image tags in k8s manifests to deploy.
DOKS Cluster: lux-k8s (LB: 134.199.138.27)
All Lux production services run on the lux-k8s DOKS cluster (do-sfo3-lux-k8s):
- lux-mainnet: 5 luxd validators (StatefulSet, ports 9630-9631)
- lux-testnet: 5 luxd validators (StatefulSet, ports 9640-9641)
- lux-devnet: 5 luxd validators (StatefulSet, ports 9650-9651)
- lux-gateway: Gateway API gateway
- lux-explorer: Lux Explorer (single Go binary, indexer+GraphQL+SPA in one process) — 5 chains: C, Zoo, Hanzo, SPC, Pars
- lux-bridge: Bridge UI + API server
- lux-mpc: MPC dashboard + API (5 MPC nodes, threshold 3)
- lux-system: hanzo-ingress LoadBalancer, lux-operator
DNS (Source of truth: k8s/dns/lux-network.yaml)
- LB:
134.199.138.27— ALL*.lux.networkservices (DNS-only, no CF proxy) - Cloud:
134.199.136.205—cloud/api.cloud/ws.cloud/web3.lux.network - Cloudflare Zone:
287bdfd07cf016bd102f6394892e4759
White-Label Explorers
| URL | Chain | Logo Source |
|---|---|---|
| explore.lux.network | C-Chain | ~/work/lux/logo |
| explore-zoo.lux.network / explore.zoo.network / explore.zoo.ngo | Zoo | ~/work/zoo/logo |
| explore-hanzo.lux.network / explore.hanzo.network / explore.hanzo.ai | Hanzo | ~/work/hanzo/logo |
| explore-spc.lux.network | SPC | unicorn |
| explore-pars.lux.network / explore.pars.network | Pars | ~/work/pars/logo |
Key Images (GHCR)
| Image | Purpose | Current |
|---|---|---|
| ghcr.io/luxfi/node | Blockchain node | exact tag + digest in deploy/lux-*/luxd.yaml |
| ghcr.io/luxfi/explorer | Explorer (single Go binary: indexer+GraphQL+SPA) | 1.2.1 |
| ghcr.io/luxfi/operator | K8s operator | v0.3.13 |
| ghcr.io/luxfi/indexer | Chain indexer | v1.0.2 |
| ghcr.io/luxfi/gateway | Gateway | latest |
In-Cluster Databases
postgres.hanzo.svc(user=hanzo) — DBs: cloud, commerce, console, gateway, hanzo, kmslux-sql-0.lux.svc— MPC API database (mpc_api)indexer-postgres.lux-explorer.svc— Indexer database
Hanzo Services: NOT on lux-k8s
All Hanzo services (IAM, KMS, Platform, etc.) run on the separate hanzo-k8s cluster.
Local Development
Docker Compose files in this repo are for local development only.
White-Label Architecture
See ARCHITECTURE.md for the canonical service topology, hostname-to-service mapping,
and namespace layout. Key points:
- All services run on lux-k8s (do-sfo3-lux-k8s), LB: 134.199.138.27
- White-label brands (Lux, Zoo, Hanzo, SPC, Pars) share backends, differ by frontend ConfigMap
- Exchange, Explorer, Cloud, Bridge, Faucet all support per-brand env var overrides
- Brand config pattern:
config/brand.tsreadsNEXT_PUBLIC_BRAND_*env vars, defaults to Lux
E2E Tests
21 spec files in e2e/ (Playwright), ~488 test cases total. Run via:
pnpm exec playwright test
| Spec | Tests | Area |
|---|---|---|
| 00-health | 27 | Node health, chain sync |
| 01-explorer | 13 | Lux Explorer UI |
| 02-exchange | 13 | DEX swap flow |
| 03-bridge | 9 | Bridge UI |
| 04-staking | 11 | Staking operations |
| 05-cloud | 25 | Cloud platform |
| 06-wallet | 13 | Wallet operations |
| 07-validator | 19 | Validator management |
| 08-validator-lifecycle | 17 | Validator lifecycle |
| 09-defi-flow | 31 | Full DeFi flow |
| 10-stats-dashboard | 15 | Stats dashboard |
| 11-wallet-ops | 20 | Wallet operations (deep) |
| 12-staking-rpc | 17 | Staking via RPC |
| 13-bridge-mpc | 15 | Bridge + MPC |
| 14-dex-contracts | 19 | DEX contract interactions |
| 15-nft-marketplace | 11 | NFT marketplace |
| 16-app-chains | 18 | App chain operations |
| 17-governance | 11 | Governance flow |
| 18-broker | 35 | Broker operations |
| 19-broker-deep | 97 | Broker deep tests |
| 20-exchange-deep | 30 | Exchange deep tests |
Plus e2e/tests/lux-cloud.spec.ts (22 tests).
Troubleshooting
Service Issues
- Ensure networks are running:
./monitoring/health_check_system.sh - Check Docker logs:
docker compose logs {service} - Verify port 9630 is open:
netstat -tulpn | grep 9630 - Run database fixes:
./fix_explorer_databases.sh
Build Failures
- Check Go version (1.26.1+ required for node)
- Clear build cache:
go clean -cache - Ensure no local replace directives in go.mod
- For JS projects: Remove node_modules and reinstall
Important Files
- Genesis Generator:
unified_genesis.py- Creates genesis files - Network Status:
docs/NETWORK_STATUS.md- Current deployment status - POA Details:
docs/POA_IMPLEMENTATION_SUMMARY.md- POA guide - Health Monitoring:
monitoring/health_check_system.sh- System health - Token Data:
genesis-*/allocations_combined.json- Account balances
Ecosystem Architecture
Lux provides the base consensus layer (L0) for the AI compute ecosystem:
Lux (L0 Consensus) → Hanzo (L1 AI Compute) → Zoo (App Layer) → Zen (Models)
Related Organizations
- Lux Network: Base L0 multi-consensus blockchain
- Hanzo AI Inc (Techstars '17): L1 AI compute infrastructure (HMM, ASO)
- Zoo Labs Foundation (501c3): Decentralized training (DSO, PoAI)
- Zen LM: Open frontier models (600M-480B params)
Full Stack Development (Hanzo Ecosystem)
For complete integrated development with AI services:
cd ~/work/hanzo/experiments/stack
make dev # Starts Router, Chat, Cloud, IAM, Search, Runtime
See ~/work/hanzo/experiments/stack/LLM.md for full documentation.
Rules for AI Assistants
- ALWAYS update LLM.md with significant discoveries
- NEVER commit symlinked files (.AGENTS.md, CLAUDE.md, etc.)
- NEVER create random summary files - update THIS file
- ALWAYS use luxfi packages, NOT go-ethereum or upstream legacy
- NEVER bump Go packages above v1.x.x
Troubleshooting
P-Chain/Info API Timeout After Block Import
Symptom: After admin_importChain, P-chain and Info APIs timeout.
Solution: Rebuild with deadlock fix (see "Critical Fix" section above).
GPU Library Not Found
Symptom: Linker errors for lux_gpu_* symbols.
Solution: Build without CGO: CGO_ENABLED=0 ./scripts/build.sh
Network Controller Crash
Symptom: lux network status shows "not running".
Solution:
lux network stop --mainnet --force
lux network stop --testnet --force
lux network start --mainnet
Block Import Fails
Symptom: admin_importChain returns error.
Solution: Write genesis state spec first, then import.
K8s manifests + auto-bump
Registry convention
All Lux images live at ghcr.io/luxfi/<service>:<tag>. Never push to GAR.
Mixing in shared upstream images (ghcr.io/hanzoai/{kms,gateway,ingress,sql,kv,static})
is allowed for shared platform components — Lux universe pins them where used.
Manifest layout
| Path | Owner | Notes |
|---|---|---|
k8s/lux-k8s/<service>/deployment.yaml |
per-service | Style reference: vote/, safe/ |
k8s/lux-k8s/validators/{statefulset,crd-resources}.yaml |
LuxNetwork CRD | Operator-managed |
k8s/lux-mpc/{luxmpc,dashboard,ingress}.yaml |
MPC | LuxMPC CRD + dashboard Deployment |
k8s/lux-ns/dex-{api,docs}.yaml |
DEX gateway/docs | |
k8s/lux-mainnet/, lux-explorer/ |
Network slices | Indexer/explorer fanout per chain |
Tag style — pin reality, not aesthetics
GHCR is the source of truth. Two known styles in use:
node(ghcr.io/luxfi/node): bare semver, e.g.1.23.31. Novprefix exists in GHCR.mpc,broker,indexer:v-prefix semver, e.g.v1.10.1,v1.5.0,v1.0.1.
Never invent the other form. Verify with:
TOKEN=$(curl -s "https://ghcr.io/token?service=ghcr.io&scope=repository:luxfi/<svc>:pull" | jq -r .token)
curl -sL "https://ghcr.io/v2/luxfi/<svc>/tags/list?n=2000" -H "Authorization: Bearer $TOKEN" | jq .
Never pin :latest in main-branch manifests. If a service has no semver yet, leave a # TODO: pin once a semver tag is published comment above its image: line.
Where a values file carries both tag: and digest:, the digest is what kubelet
pulls and the tag is only a label. Bumping one without the other deploys the old
bits under the new name — the rollout succeeds, the pods are healthy, and the
footer reports a version that is not what is running. Do not advance them by
hand: deploy/pin.sh resolves both from one registry read and writes them in one
edit, so there is no second lookup for them to drift against.
The release train
Service repo: build → push ghcr.io/luxfi/<svc>:<tag> → dispatch <svc>-released
universe: .hanzo/workflows/image-published.yml → deploy/pin.sh → commit to main
cd.hanzo.ai: git generator over deploy/*/*.yaml → reconcile
.hanzo/workflows/image-published.yml is the only listener and moving the pin is
the only thing it does. Which files move is DERIVED — pin.sh --which <repo>
reads every values file whose image.repository is the one that was published.
The per-service table this replaced named four k8s/ paths that did not exist,
and k8s/ is not read by CD anyway.
Dispatch types wired: image-published, plus <svc>-released for
mpc, mpc-api, mpc-dashboard, dex-gateway, dex-docs, dex, broker,
cex, bank-api, bank-admin, bank-dash, kms.
payload: { service, tag, image? } — image is a REPOSITORY, never
repo:tag, and defaults to ghcr.io/luxfi/<service>. pin.sh reads the tag
convention (v-prefixed or bare) out of the values file, so a sender does not
have to know it. It refuses a non-semver version, an unpublished tag, and a
version older than the current pin (PIN_ROLLBACK=1 to go back deliberately).
ghcr.io/luxfi/node is refused outright: luxd is promoted only by
scripts/roll_luxd.sh, which enforces quorum, all-chain bootstrap and the
staged-network order that a tag bump cannot express.
IAM is upstream (ghcr.io/hanzoai/iam); not in the dispatch list.
Known drift and TODOs
| Service | Drift | Action |
|---|---|---|
dex-gateway, dex-docs, market, wallet, pricing, explorer |
No semver in GHCR | TODO comments left inline; cluster runs :latest. Publish tags from source repos. |
broker, cex, dex (matching engine) |
No live deployment in any Lux cluster | Dispatch types reserved; manifests to be authored once a service goes live. |
bank-api, bank-admin, bank-dash |
Live but commit-sha pinned | Manifests authored at sha; switch to semver once Bank repo cuts releases. |
node |
Universe pins 1.23.31, GHCR latest stable 1.24.28 |
Validate then run scripts/roll_luxd.sh once mainnet is ready to advance. |
| ~100 values files | Concrete version, no digest | A tag names a release but a registry tag is mutable: zoo/exchange-api was pinned at bytes 2.3.34 no longer meant. pin.sh --verify lists them. |
explore-fe-pars (x3), hanzo/lux-admin |
pin.sh --verify cannot read them |
403 from ghcr.io/parsdao with a luxfi token; no DO registry credential. Not defects — grants. |
chart app |
ApplicationSet pins 0.1.11; 0.1.13 is published | 3 values files use kms.hostAPI/credentialsSecret, which 0.1.13 removed for a ZAP transport, and its KMSSecret group changes to kms.hanzo.ai/v1. Cluster-side prerequisite — see deploy/README.md. |
Decomplection audit (2026-05-29) — lux-system orphan
Live cluster do-sfo3-lux-k8s ns lux-system carries an orphan status
Deployment that violates several rules and must be removed:
- Image policy violation:
ghcr.io/hanzoai/status:latest— no:latestper CLAUDE.md "no floating tags". - Second pod stuck: 5+ day ContainerCreating because it mounts a
ConfigMap
status-configthat has no source-of-truth. - Duplicate concern:
status-lux(sourced fromk8s/status/deployment.yaml) serves the same role and is correctly pinned by sha.
The orphan + its ConfigMaps (status-config, status-frontend,
status-brand, status-logo) and PVC (status-data) have no source
in this repo — deployed by hand 92d ago before the lux/hanzo brand
split. They are safely removable.
Cleanup script: k8s/status/orphan-cleanup.sh. Production-impacting;
confirm before running:
KUBECTL_CONTEXT=do-sfo3-lux-k8s ./k8s/status/orphan-cleanup.sh
Recorded outside this entry — k8s/status/kustomization.yaml added so
the canonical status-lux stack is Kustomize-applyable.
Decomplection audit (2026-05-29) — luxd-startup bash bag
k8s/lux-mainnet/luxd-startup.yaml inlines a single bash script that
braids 7 distinct concerns:
- staking-key copy
- plugin install
- per-chain
upgrade.jsonwrite - chain-aliases
aliases.jsonwrite - bootstrap-peer DNS computation
- one-time RLP imports (mainnet only)
- luxd flag assembly
The operator OWNS the StatefulSet (ownerReferences: LuxNetwork) but
does NOT manage the command/args/volumeMounts for /scripts —
those survive on the live spec via SSA from a hand-applied
StatefulSet patch. Dual-source pattern; the hand patches and the
operator reconciliation co-exist.
Decomplection landed in luxfi/operator@decomplect/luxnetwork- declarative-startup (commit d5b3d26):
- Two of the seven concerns moved to CR fields:
LuxChainSpec.Aliases []stringLuxChainSpec.UpgradeConfig string
- One new init container slot:
chain-setupbetweenseed-restoreandstartup-gate
- New manifest builder:
manifests.BuildChainSetupInitContainer(10 tests)
- Reconciler wired to emit chain-setup from CR-declared state
Migration plan (production-impacting, NOT yet run):
- Land operator decomplection PR → bump operator image
- Add
aliases:andupgradeConfig:to chain entries ink8s/lux-mainnet/network.yaml - Wait for operator reconcile to write
chain-setupinit container - Verify aliases.json + upgrade.json on disk match what luxd-startup.yaml would have written
- Remove inline
cat > aliases.jsonand per-chain upgrade.json blocks fromluxd-startup.yaml - After multiple successful pod restarts, scrub remaining bash concerns (bootstrap-peer DNS → operator-emitted env var; plugin install → init container with declarative VMID→objectKey map; etc.)
The five remaining concerns (staking-key, plugin install, RLP import, bootstrap-peer DNS, luxd flag assembly) are NOT YET decomplected. Tracked for follow-up; not landed this session.
Fresh-bootstrap primary (testnet + devnet) — 2026-06-02
testnet and devnet had been wedged with classic BLS-key drift:
node has BLS key 0xa9503da5... but is registered to the validator set
with 0x95bc37b9... (testnet)
i.e. the luxd-staking Secret signer.key didn't match the BLS pubkey
encoded in the lux-{net}-genesis ConfigMap → 0 peers, 0 progress.
The fix is a clean rebuild of (Secret + genesis ConfigMap + PVC wipe),
codified as a re-runnable procedure. Source of truth for the
5-validator key set: ~/work/lux/keys/node{1..5}/{staker.{crt,key}, bls/signer.key, ec/private.key} — the canonical local validator
identities used by the genesis CLI's KEYS_DIR discovery path. Same
keys for both testnet and devnet; per-env isolation comes from the
different networkID (2 vs 3) baked into the genesis JSON.
Source-of-truth genesis ConfigMaps now committed at:
k8s/lux-test-k8s/testnet/genesis.yaml—lux-testnet-genesis(networkID=2, C-Chain ID 96368)k8s/lux-dev-k8s/devnet/genesis.yaml—lux-devnet-genesis(networkID=3, C-Chain ID 96367)
Both genesis blobs:
- 5
initialStakersfrom node{1..5} certs (NodeIDs deterministic viaNodeIDFromCertover the constant-time PEM cert bytes) - 5 allocations, each with
unlockSchedule: [{amount:50T, locktime:0}](CRITICAL — empty unlockSchedule yields zero validator weight at genesis parse and the node refuses to boot) cChainGenesissourced verbatim from~/work/lux/genesis/configs/{testnet,devnet}/cchain.json(canonical)- No L2 chains (
chains: []) — brand L2s (Hanzo/Zoo/SPC/Pars) re-created via CreateChainTx as a separate follow-up (that follow-up was dropped 2026-06-27; brands run their own network — see "Zoo and Hanzo are not chains on the Lux primary network")
Genesis CLI gotchas:
LoadKeysFromDirscans EVERY subdirectory of KEYS_DIR including.git/,cmd/,proofs/. Use a clean staging dir (e.g./tmp/lux-keys-clean/) containing ONLY the fivenodeN/folders.buildConfigFromKeyInfosemitsunlockSchedule: null— post-process with jq to add the matching[{amount: X, locktime: 0}].-cchain ~/work/lux/genesis/configs/{net}/cchain.jsonis required to get the canonical EVM chainID (96368 testnet / 96367 devnet) — without it the C-Chain defaults to chainID == networkID.- devnet startup script previously hardcoded
--track-chains=nQCV...(old L2 ID). After fresh-bootstrap the L2 doesn't exist, so C-Chain wouldn't bootstrap. Patched live CM to--track-all-chains=true(testnet pattern).
NodeID ↔ key index (deterministic):
| pod | NodeID | BLS pubkey prefix |
|---|---|---|
| 0 | NodeID-Jc1ACAWeKDZR4N5PgxNsPrrcKrKMTh4ct |
0xb57029264c… |
| 1 | NodeID-CTyFvG1HYdcMLqdmKwqA8EFPRQdmrNat3 |
0xa8c919018c… |
| 2 | NodeID-Hvsc6vanABE3ufcMhpMVEMVGpZ68DhoMb |
0x8311f8eb15… |
| 3 | NodeID-44e37hMkUbjpoWsJrWcvBB4kcMK3ab8qg |
0xa1efbd8916… |
| 4 | NodeID-HcFvgYUxXJx4gzQ7Sgsj8grYK6FDEgBrr |
0xa07f54a097… |
Verification probe (P + C bootstrap on all three networks):
for ctx_ns_port in \
"do-sfo3-lux-k8s lux-mainnet 9630" \
"do-sfo3-lux-test-k8s testnet 9650" \
"do-sfo3-lux-dev-k8s devnet 9650"; do
set -- $ctx_ns_port
for chain in P C; do
kubectl --context=$1 -n $2 exec luxd-0 -- \
curl -sS -X POST -H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"info.isBootstrapped\",\"params\":{\"chain\":\"$chain\"},\"id\":1}" \
http://localhost:$3/v1/info
done
done
All six results expect isBootstrapped: true. Confirmed 2026-06-02.
Brand L2 chain registration on test+dev primaries — 2026-06-02 (closes #168)
Superseded 2026-06-27. The record below is what was done and is kept for provenance; it is not the model. Brands run their own network — see "Zoo and Hanzo are not chains on the Lux primary network".
The 4 brand L2 EVMs (hanzo/zoo/spc/pars) are now registered on the fresh
testnet + devnet primary networks via IssueCreateChainTx (P-chain
canonical tx, codec name unchanged from upstream). One pair = one chain
across both envs (the parent networkID is per-env: testnet=2, devnet=3;
the per-chain validator-network (CreateNetworkTx ID) is per-(env, brand)).
Tool: ~/work/lux/genesis/cmd/bootstrap-chain (now accepts --key-file <path-to-32-byte-hex-secp256k1> in addition to MNEMONIC+BIP44).
Tool source cmd/bootstrap-chain/main.go — modeled on
lux/node/wallet/network/primary/examples/bootstrap-hanzo/main.go and
extended to be data-driven over --track-chain-ids.
Wallet patch: lux/node/wallet/network/primary/api.go now fails-soft on
"no X alias" — required because testnet+devnet primaries run in P-only
mode (no XVM in platform genesis). FetchState degrades the X-chain
context to ids.Empty instead of erroring; the chain-pair UTXO scan
skips X entirely when X is unavailable. Drop the local replace in
genesis/cmd/go.mod once luxfi/node ships a tag carrying this change.
Per-env, per-brand registration
Deployer (shared test+dev):
- private key:
~/work/lux/keys/deployer-test-dev/private.key(deterministic SHA-256 of"lux-test-dev-l2-bootstrap-deployer-2026-06-02") - test P-chain:
P-test19zc4fcupelfkue4z9wujxmu2tv9505q27ph88j - dev P-chain:
P-dev19zc4fcupelfkue4z9wujxmu2tv9505q20pelm6 - EVM addr (shared):
0x28b154e381cfd36e66a22bb9236f8a5b0b47d00a
Funded via a 6th allocations entry in lux-{testnet,devnet}-genesis
(100B nLUX = 100K LUX per network, unlockSchedule:[{amount, locktime:0}],
NOT in initialStakedFunds so the UTXO is spendable at genesis).
Adding this allocation required a second wipe-and-rebootstrap of the
primaries (fresh #186 rebuild was funded entirely into stake). Genesis
ConfigMap reapplied → StatefulSet scaled to 0 → PVCs deleted → scaled to
3 → re-bootstrapped. P + C bootstrapped on all 3 pods per env in ~75s.
Snapshots from #186 retained for forensic comparison (data-luxd-{0..2}-20260602).
Common tx parameters per chain:
- vmID:
mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6(canonical EVM plugin) - chain authoriser: own per-chain CreateNetworkTx (L2 anchor pattern; the bootstrap-chain tool issues CreateNetworkTx first, then CreateChainTx against that network ID)
- validators added per chain: all 5 primary validators via IssueAddChainValidatorTx (chain start = +60s, end = primary's earliest stake-end minus 1h)
Per-env inventory (also committed at
k8s/lux-{test,dev}-k8s/{testnet,devnet}/chains-inventory.json):
| brand | env | blockchainID | per-chain networkID | evmChainID |
|---|---|---|---|---|
| hanzo | test | 2Cgj3Re98MfRB44SneJcfF4K6uctP2shAZKayrLpAZenzyBH7d | 2NLMq51Kz7HzTDvu42r9CwfE5wxhmPoTpF8NAkYJwnVHGjqwFQ | 36962 |
| zoo | test | 2UHBMA2XfiB1azgNANbby9oexbMV4aQ5JMDuLPEGvMNSLB2BrN | Wdfqb7qunsabQCxfsLnS6RCApdZxij2SXcF9oTLZR2SoYYgHH | 200201 |
| spc | test | 2uKb33jCcZWCBurRG6UYtvgKd5reycscN6yXToHjhLUWzkdXYc | m7TyVpMgyi99dRF74ywP9epXY1Ta3XpNMcV6U8yA8zhqKfjZG | 36910 |
| pars | test | WhWbGHTi8SFRjHrTYDWSxhPFgUvQW2oiwzNvHgLxEdeWVS9ca | FuRQspw3hcqtWQhD8nUNTiC9rb9KTeKFaZvrKHEph8iUfejfz | 494950 |
| hanzo | dev | 2gSYzcV4Jo3ATAPrsJnjhmUjPVtmMNFqEMCpDjZ2hroAtzz3Ko | sz2oFLPvGz7KQ7BHrs2tcY5WTFwP9ad2UgorsCyEM4jc563jn | 36964 |
| zoo | dev | GP4ZLRFjoTcZSmuhPvJWuvRdHC4ZhiYp5M2RSPQxc33sNjkuD | UXckap5QxjViKuKePQp6mwaWrD19is7foFsqrRDfJKDc85HGo | 200202 |
| spc | dev | 2bgKkULQx7nuNPD5tVKfHjMiaqKbQWykwNVECKAL4EDuTmHGyE | EMQBtaiRSoerFsnyiHpU2F1dRfkzGw6KU6NtTA1C4E9JG4ArH | 36912 |
| pars | dev | YNcHsEAEy4tRjGi4TmEvgKXk6bykh7BSRpeCRjWJKNkYkRdB6 | 2kEVjgH5AquPxVFvogoi4LbtQyRg5WfGXH2XqphonzNfCpcBzx | 494951 |
Verification (24/24 pods × chains: isBootstrapped: true):
for cid in <chainID>; do
for pod in 0 1 2; do
kubectl --context=do-sfo3-lux-{test,dev}-k8s -n {testnet,devnet} exec luxd-$pod -- \
curl -sS -X POST -H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"info.isBootstrapped\",\"params\":{\"chain\":\"$cid\"},\"id\":1}" \
http://localhost:9650/v1/info
done
done
eth_chainId confirms canonical map on every L2 (hex matches dec from
the LP-182 EVMChainID map in luxfi/genesis).
Followups (separate tasks)
- Each new EVM chain currently sits at block 0 —
--evm-heartbeat-keyflag of bootstrap-chain can roll block 1 if funded. Hold for downstream contract deploys to roll their own blocks. - Gateway path-based routing (
/testnet/v1/chain/<brand>/rpcetc) ink8s/lux-gateway/gateway.yamlstill points at OLDlux-testnet/lux-devnetnamespaces with stale chainIDs. Update to route to thelux-testnet/lux-devnetnamespaces on lux-k8s with new blockchainIDs. (Mainnet entries are correct.) NOTE: the lux-test-k8s / lux-dev-k8s CLUSTERS no longer exist — see the consolidation section at the end of this file. - kms-operator deployment in test+dev clusters → migrate file-on-disk
~/work/lux/keys/deployer-test-dev/private.keyto KMS-stored per-env deployer secret. Same key, encrypted-at-rest path.
Historical network upgrade record — node version + precompiles (2026-06-23)
This section records the June 2026 migration and is not an operating procedure.
Use RUNBOOK.md for the current, sole deployment path. At the time of this
record, all three networks ran ghcr.io/luxfi/node:v1.30.40
(latest PQ crypto: consensus v1.25.19 ConsensusCert, pulsar v1.1.5, magnetar
v1.2.3, corona v0.7.9, threshold v1.9.9) with the migrated DEX precompiles.
Historical declared state
| File | Controls | Live-of-record |
|---|---|---|
k8s/lux-{net}/network.yaml |
LuxNetwork CR image.tag (node version) |
v1.30.40 |
k8s/lux-{net}/luxd-startup.yaml |
luxd-startup ConfigMap → C-Chain upgrade.json (precompile activations) |
migrated |
These are captured from the live cluster so a rebuild / operator reconcile reproduces the exact deployed state. Never hand-edit the live configmap without mirroring it here.
DEX precompile migration (dexConfig → granular, for full C↔D compat)
The monolithic dexConfig (old DEX at 0x0400) was split in node ≥ the
Uniswap-v4 refactor into four granular precompiles at 0x9996-0x9999:
dexPositionManagerConfig (0x9996), dexStateViewConfig (0x9997),
dexQuoterConfig (0x9998), dexSettleConfig (0x9999). Each config is just
{"blockTimestamp": <ts>} (stateless adapters; the old params —
protocolFeeController/maxPools/flashLoans/hooks — are now on-chain governance).
- Activation ts =
1766708400(4:20pm PST Dec 25 2025) — matches the 29 other precompiles already at that ts in the chain config. (The universe network-start table says1766704800/PDT; that's a separate 1hr-earlier metadata value — precompile activations use1766708400.) precompileUpgradesMUST be ascending by blockTimestamp — insert the new entries in sorted position, never append at the end (coreth rejects out-of-order:timestamp < previous timestamp).
Retired procedure (do not use)
- Edit declared state here: bump
network.yamltag:and/or edit theupgrade.jsoninsideluxd-startup.yaml. - Deploy via platform.hanzo.ai (PaaS-driven operator reconcile) — or the
deploy-k8s.ymlworkflow. NEVER hand-kubectl set image/applyad-hoc. - Coordinated cutover when binary↔config are incompatible: a node version
that renames precompiles (e.g. v1.30.3
dexConfigvs v1.30.40 dex* ) CANNOT roll one-pod-at-a-time through one shared configmap — the configs are mutually exclusive. Switch image and config together and restart all validators (brief C-chain downtime); verify C-chain re-bootstraps to full height (= replay-safe) before declaring done; auto-rollback to the prior image+config if it doesn't. - DNS / public hosts are Cloudflare (zone
287bdfd07cf016bd102f6394892e4759for*.lux.network).*.lux-dev.network/*.lux-test.networkare separate CF zones (scoped token needed). Explorer frontends (explore.lux-test.network) are CF-served Next.js apps, NOT in-cluster ingresses — devnet'sexplore.lux-dev.network(404) needs the same CF Pages/frontend deploy, not a k8s ingress (the in-cluster devnetexplorerpod serves nothing on :80/:3000).
Verify all 3 (P/X/C bootstrap + version)
for n in "lux-mainnet api.lux.network" "lux-testnet api.lux-test.network" "lux-devnet api.lux-dev.network"; do
set -- $n; echo "$1:"; for ch in P X C; do
curl -s -X POST -H 'content-type:application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"info.isBootstrapped\",\"params\":{\"chain\":\"$ch\"}}" \
"https://$2/v1/info"; done; echo; done
Confirmed 2026-06-23: all P/X/C isBootstrapped:true; mainnet C-chain at
1,082,800 blocks preserved through the v1.30.3→v1.30.40 coordinated cutover.
Last Updated: 2026-06-23 Note: This file should be committed. Symlinks (CLAUDE.md, GEMINI.md, etc.) are for local use only.
Lux as a Hanzo Cloud customer org — DOKS + billing (2026-07-02)
Lux runs as a first-class customer org on the shared Hanzo Cloud control
plane (platform.hanzo.ai / console.hanzo.ai), NOT a bespoke path. This is how
lux-k8s and future Lux compute are managed + billed centrally.
- Identity:
[email protected]is the lux-org owner in IAM (lux.id). Logging in to platform.hanzo.ai as a lux user auto-creates theluxorg + owner membership + wallet in the platform DB (testedupsertUserFromIamflow). - DOKS: per-org DOKS provisioning/attach is enabled (Hanzo side wired
PAAS_DO_API_TOKEN2026-07-02).lux-k8s(DOKS id04c46df5-5986-429a-9c7f-3f11a81c8539, since retired — the fleet is addressed to the hanzo-cd cluster NAMElux-cluster, not to an apiserver URL, so a replacement is registered once there and nothing in this repo changes) attached to the lux org viaPOST /v1/org/{luxOrgId}/cluster/attach; new clusters viaPOST /v1/org/{luxOrgId}/cluster. Full runbook: hanzo/universe LLM.md "Per-org DOKS provisioning". - Billing: lux org gets an
organization_wallet(seed $10,000 credits; admin top-up via admin.hanzo.ai). DO-resell usage meters DO cost → commerce → wallet, tracked + invoiced like any paying customer. - Console: visible to the lux-org user at console.hanzo.ai (org-switch to lux) today; the branded home is console.lux.cloud (white-label, pending the SSR-brand-host fix + DNS cutover — see hanzo/universe white-label notes).
Branded web3-surface regression — e2e/branded/ (2026-07-20)
The durable "is every public surface live · functional · branded" gate. 26
surfaces across Lux/Zoo/Hanzo/Pars (explorers, wallets, id, votes, work boards,
bridges, safe, clouds, exchanges) × 4 breakpoints (390/768/1280/1920) = 104
cells, real Chrome/Blink (channel:'chrome'), read-only (no txns, no
wallet-connect, no auth — the login form is asserted, never submitted).
- Run:
npm run test:branded(configplaywright.branded.config.ts, breakpoints = projects, custom matrix reporter). Screenshots →~/Desktop/lux-shots-e2e/<surface>-<bp>.png; matrix →branded-report/matrix.{md,txt,json}. Wired into CI as thebrandedjob in.hanzo/workflows/e2e.yml(installs the chrome channel, uploads the matrix). - Per cell (owner's 6-point rubric → 8 checks): HTTP 200 · branded
<title>+favicon+zero foreign brand in<head>(the white-label invariant — body chain references are legit, only the head gates) · no horizontal overflow · no app-origin console errors (3rd-party telemetry + benign resource-4xx allow-listed) · key control visible · brand font (broken Times/serif fallback fails; accent is an observational note). - Gate vs flag (the model):
e2e/branded/surfaces.tsis the ONE source of truth; the spec+reporter are a pure function of it. Alivesurface gates every check unless a documentedflag(with areason+flipcondition, optionallybps-scoped) demotes it to ⚑.down/foreign-domainpostures never gate. A flagged check that starts passing is reported STALE so the table self-cleans. Only a NEW un-flagged failure turns the run red ✗. - Baseline 2026-07-20: 67 pass · 37 flagged · 0 regressions. Documented flags
(each carries a flip): bridge zoo/pars/lux Times-font fallback + zoo/pars head
<meta description>still "…Lux Network." (white-label incomplete) + bridge-zoo CORS→bridge-api.lux.network; zoo/hanzo vote safe-client console errors; zoo.cloud @390 ~110px overflow; exchange.lux Vite-preload/$0-quotes + @390/@768 marketing-band overflow. Owner-gated (posture down/foreign): hanzo.cloud = JP company, hanzo.exchange = GoDaddy parking, lux.exchange = SSL/SNI-blocked. Confirmed-fixed since the 2026-07-18/19 sweeps: wallet.zoo (was 100% Lux) now Zoo-branded; zoo.vote head-leak gone; all 4 work boards + safe.lux white-labeled; explorers no longer overflow @1280.
Cluster consolidation — verified inventory + teardown plan (measured 2026-07-27)
One cluster per org, one NAMESPACE per blockchain network. The per-network
part is already done and needs no work: doctl kubernetes cluster list returns
8 clusters and none of them is a per-network split. mainnet/testnet/devnet is
a namespace everywhere it exists — lux-{mainnet,testnet,devnet},
zoo-{mainnet,testnet,devnet}, pars-{mainnet,testnet,devnet}. The old
per-network CLUSTERS are already gone from DigitalOcean; only the stale
k8s/lux-dev-k8s/ and k8s/lux-test-k8s/ dirs (and the Followups note above,
which still names them as if live) survive them.
Fleet as scanned by clients/admin/infra (the code behind GET /v1/admin/infra):
8 clusters · 44 nodes · 183 volumes · 10 load balancers · $4,274.10/mo, complete=true.
| Cluster | Nodes | Pods | What it actually runs | Verdict |
|---|---|---|---|---|
| hanzo-k8s | 17 | 370 | the platform; LB ingress 129.212.164.5 |
keep — org cluster |
| lux-k8s | 10 | 343 | lux-{mainnet,testnet,devnet} + apps | keep — org cluster |
| zoo-k8s | 4 | 91 | zoo-{mainnet,testnet,devnet} | keep — org cluster |
| pars-k8s | 3 | 35 | parsd-mv 5/5 ghcr.io/luxfi/node:v1.36.2 + 4 app deploys; 0 ingresses |
collapse → lux-k8s |
| osage-k8s | 3 | 27 | osaged-mv 5/5 ghcr.io/luxfi/node:v1.36.2; 0 ingresses |
collapse → lux-k8s |
| bootnode-k8s | 2 | 40 | 8 deploys, ghcr.io/hanzoai/bootnode; 29 live hosts; ingress-nginx |
collapse → hanzo-k8s bootnode |
| adnexus-k8s | 3 | 46 | 14 deploys; 10 *.ad.nexus hosts; ingress-nginx |
collapse → hanzo-k8s adnexus |
| hanzo-dev-k8s | 2 | 56 | dev tier; 70 ingress hosts, mostly dormant | collapse → hanzo-k8s hanzo-devnet |
Removing the five candidates retires ~13 nodes (3+3+2+3+2) — at DO list price
(s-4vcpu-8gb $48, s-2vcpu-4gb $24) about $504/mo of droplets plus their
load balancers.
Done (safe half)
pars-mainnet, pars, osage-mainnet created on lux-k8s and declared here.
pars-testnet / pars-devnet deliberately NOT created — both are empty on
pars-k8s (0 deploy / 0 sts / 0 pods). bootnode, adnexus and hanzo-devnet
already exist on hanzo-k8s.
Not done — needs a human decision before anything is deleted
- pars / osage first (lowest risk: zero ingresses, so no DNS step). Move the StatefulSet + its PVCs; validator identity keys live in KMS, so re-key nothing. Capacity is the real constraint — lux-k8s already runs 345 pods on 10 nodes at 27–87% memory, so add a node pool BEFORE the move, not after.
- bootnode-k8s is not idle. It answers 29 production hosts (
lux.cloud,api.lux.cloud,bootno.de,zoo.cloud,api.lux.network, …) and several of them are ALSO declared on hanzo-k8s nsbootnode— dual-homed. Public DNS is Cloudflare-proxied, so the origin cannot be read fromdig; the Cloudflare records must be read per host before its LB (134.199.136.205) is retired. - hanzo-dev-k8s declares production identity hosts (
lux.id,pars.id,bootno.de,kms.*,s3.*) but does NOT serve them:lux.id/pars.idresolve to 129.212.164.5 = the hanzo-k8singressLB. The declarations are dormant duplicates; still verify per host before deleting. - adnexus-k8s is a clean lift-and-shift of 14 deploys + 10 hosts onto the
already-existing (and currently empty) hanzo-k8s
adnexusnamespace. - Both bootnode-k8s and adnexus-k8s run ingress-nginx, which is banned — collapsing them onto hanzoai/ingress removes it as a side effect.
- Correlated-failure note: putting pars/osage validators on lux-k8s makes one cluster a shared failure domain for three sovereign L1s. Sovereignty is a network-layer property and is unaffected, but availability is not — this is the trade to accept explicitly, not by accident.
No cluster is deleted by any commit in this repo. DOKS deletion is effectively irreversible; each teardown is its own reviewed change after its migration is verified green.
One repository: github.com/luxfi/universe (2026-09-04)
github.com/luxfi/universe is the canonical remote. git.hanzo.ai/lux/universe
is unreachable; its lineage was merged into GitHub's in one commit, and
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.
from here on:
- A static site is three objects and no image: a
staticFilesMiddleware, the route that names it, and the endpoint-less Service the route schema requires (deploy/lux-credit/lux-credit.yamlis the reference). The DigitalOcean-era pod declarations of those sites are gone; their last image pins are recorded in the merge commit's message. cd.automated: trueonly on a file verified to run on the current cluster. Every pod-based file isfalseuntil that is shown; a newer pin does not opt a file back in.- A chain answers under
/v1/chain/<alias>/rpcand the gateway asks luxd for health at/v1/health/ops/liveness; every declaration and script says so. - luxd moves only through
scripts/roll_luxd.sh, which writes the tag in the spelling the values files already use (bare1.36.181) and one digest to all three fleets. k8s/never redeclares whatdeploy/declares. A k8s manifest that duplicated a values file is deleted, not merged back.
Zoo and Hanzo are not chains on the Lux primary network — measured 2026-09-09
Asked to create zoo and hanzo with CreateChainTx on Lux mainnet and bootstrap
them. Neither was created. Three independent reasons, each measured against the
live fleet on evo (10.0.0.21, five validators on 9730–9770,
ghcr.io/luxfi/node:v1.36.181, --network-id=1).
They already exist, elsewhere, with their own identities. hanzod-mv-0..4
(ns hanzo-mainnet) serves eth_chainId 0x9063 = 36963 with
info.isBootstrapped(C)=true, on its own primary network 36963 — its genesis
ConfigMap hanzod-mv-genesis is a primary genesis (networkID: 36963,
initialStakers, cChainGenesis.config.chainId: 36963), not a CreateChainTx
blob. Zoo mainnet's C-chain is 23hNkQEfXaGfVVJjDPEPGgvHNfGW5BXWx47kxe67fn1qgL2fsP
on sovereign network 200200, carrying 13,348 blocks (genesis 0x7c548af4, tip
13,347 0x891a539c, 729 settlements) — see zoo/universe LLM.md, cut over
2026-07-14. Creating either on the Lux P-chain mints a second, competing chain
identity for the same brand and abandons that history.
The primary network's chain set is closed to brands by decision.
brand_sovereign_node_one_way (2026-06-27): luxd runs the Lux primary network
only. cmd/bootstrap-chain enforces it — the names hanzo/zoo/pars/spc/osage are
refused before any P-chain spend. k8s/lux-mainnet/chain-aliases.yaml records
the same finding: the brand aliases pointed at chains never created.
The P-chain has no JSON-RPC surface left, so the tooling cannot reach it.
platformvm.CreateHandlers returns exactly one handler, at /ops
(node/vms/platformvm/vm.go) — the base is deliberately unserved. Measured
through luxfi/[email protected], the client path both tools use:
platform.getHeight → /v1/chain/P → 404 (also via api.lux.cloud)
info.getNetworkID → /v1/info → 404
primary.FetchState → received status code: 404
createchaintx and bootstrap-chain both open with primary.MakeWallet, which
begins at FetchState, so neither gets past its first network call. The live
surface is typed operations under /v1/chain/P/ops (OpenAPI 3.1 at
/ops/.well-known/openapi.json): GET /blockchains, GET /utxos, POST /tx
taking already-signed bytes. Reaching the P-chain from Go again means a client
that speaks that, not a newer path string.
What the primary network carries, read from /v1/chain/P/ops/blockchains. All
eleven sit on net 11111111111111111111111111111111LpoYY; the P-chain itself is
11111111111111111111111111111111P. No T — it became F and M.
| chain | blockchain id |
|---|---|
| C | TZEa3jDMCY5iQWqm5brNhqdszHza3e2qvzSGCC63uQNbVA3z3 |
| X | 2fdnRbzs9sWj5cVqny2sdShXzZXgvm96DyzvzgB2Z7j3VKZukd |
| A | 2NXHomv4gbu8i6JTqALHvcbdeb8gb8r7jzbxqXvqZRd1UzorhF |
| B | JNF8xWg2eMam5nAq9MQaE9LuXYo9Ccv2WcY8hEts7bDdmNGtJ |
| D | 1AFS4GPsBYrzG3KLvjEfJfWAcs5AgTs2XfFoG4xBQed95p3tT |
| F | Ec9QkdmFnmhPSpuvXenwQ798mFH9gYCgJjy6V4Wfg336wFaX1 |
| G | 24C9zm36x43T7LqcaKF1ikHxSeuQeTXnstzi5Gwh2apo18rXNE |
| K | 2sK7efGAyZJcfLMKPzHg39NPJnL4SwYmFUsPDHMYVLMVYqP4Fw |
| M | 2erizn2pu4gbafsFzbyfqwzhkR2RT1qFdwr7kKKwn5T3BH3wW8 |
| Q | 2wiiMEaeLPnxQH3gs5GU1QWCBakE4axd9rJgAqwzkWfr5BwhyM |
| Z | ddz6d4PFUhuxzYdLDCiKyMfcxsuboT67KqQ7mwPt9baudAoKs |
Only C is an EVM: eth_chainId 0x17871 = 96369, head 0x3b = 59 on all five
validators. Aliases resolve, but only under /ops — /v1/chain/P alone is a
404 and /v1/chain/P/ops/blockchains is a 200. The P-chain's /height answers
204 with no body, which is why height reads look empty.
Nothing on this P-chain can pay a fee anyway. All 100 genesis allocations carry
initialAmount: 0; allocations 0–4 are initialStakedFunds and hold nothing
until their stake ends (~2026-12-12). Slots 5–98 each hold 3,500 LUX unlocked
against 46,500 stakeable-locked, and spending one needs the master mnemonic at
BIP-44 m/44'/9000'/0'/0/<slot>, which lives in KMS and was not on this machine.
What Zoo mainnet needs here instead
zood-mv (ns zoo-mainnet) is declared and scheduled but has never started:
zood-mv-0 has been Init:0/1 since it was created, on three missing objects —
ConfigMaps zood-mv-genesis and zood-mv-startup, and Secret zood-mv-staking.
The startup script is already committed at k8s/zoo-mainnet/zood-mv-startup.yaml
(--network-id=200200, /genesis/genesis.json + /genesis/upgrade.json); the
other two are not.
The genesis is the one to be careful about.
zoo/universe/configs/genesis/mainnet/genesis.json is an EVM genesis (chainId
200200) with a networkID field added, which is not the shape --genesis-file
wants — compare hanzod-mv-genesis, which is a primary genesis carrying its EVM
under cChainGenesis. Zoo needs the same: a primary genesis for network 200200
with five stakers, that EVM blob verbatim as cChainGenesis, and the existing
upgrade.json. Anything else changes the genesis hash and orphans the history.
Two facts to carry into that work. Zoo's canonical genesis hash is
0x7c548af47de27560779ccc67dda32a540944accc71dac3343da3b9cd18f14933; a genesis
that does not reproduce it is a different chain. And zoo/universe LLM.md
records that admin_importChain of the RLP diverges at block 800 on both
v1.34.9 and v1.36.2 — the state moves by direct zapdb copy of the coreth C-db,
not by RLP import. ~/work/lux/state/rlp/zoo-mainnet/zoo-mainnet-200200-0-13347.rlp
(14.7 MB, blocks 0–13347, sentinel hashes matching the canonical tip) is the
only copy of that history on this machine; the data-zood-0 PVC and its
snapshot were on the DigitalOcean cluster.