The primary-network chains answer to either case; one spelling beats two. /v1/chain/c/rpc, /v1/chain/p, /v1/chain/x across the estate. Co-authored-by: Hanzo AI <[email protected]>
20 KiB
LLM.md — Zoo Universe
Canonical guide for AI tooling working in ~/work/zoo/universe.
What this repo is
The Zoo brand's declaration of its sovereign-L1 surface. One source of
truth — chain.yaml — drives every operational verb. K8s manifests
under k8s/ deploy the brand services to do-sfo3-zoo-k8s. A 5-line
Makefile is the optional convention; the underlying interface is the
lux CLI.
Sovereign L1 model (2026-06-16)
Zoo runs its own primary network with its own validator set. C-chain is Lux Network's primary EVM — Zoo has Zoo EVM. Per-env primary networkID equals per-env EVM chainID (sovereign-L1 rule).
| Env | networkID | httpPort | RPC |
|---|---|---|---|
| mainnet | 200200 | 9700 | https://api.zoo.network |
| testnet | 200201 | 9710 | https://api.zoo-test.network |
| devnet | 200202 | 9720 | https://api.zoo-dev.network |
| localnet | 200203 | 9730 | http://127.0.0.1:9730/v1/chain/c/rpc |
Ports are explicit (httpPort + stakingPort in chain.yaml). No
port+1 implicit rule. No collisions across envs.
CLI grammar — lux <verb> <brand>/<env>
The lux CLI reads chain.yaml files discovered under $LUX_BRAND_PATH
(default ~/work/{lux,zoo,hanzo,pars,osage,adnexus}/universe) and
resolves each <brand>/<env> to a RuntimeProfile (networkID, httpPort,
stakingPort, dataDir, logDir, genesisFile, snapshotDir, snapshotName,
serviceLabel). Every verb consumes the same record.
lux ctx # list discoverable brand/env tuples
lux ps # for each tuple: probe port → up/down/foreign
lux up zoo/devnet # boot luxd in K=1 PoA mode (foreground)
lux down zoo/devnet # identify by port probe + networkID match, SIGTERM
lux snap zoo/devnet # tar.zst the data-dir into snapshotDir
lux cycle zoo/devnet # boot → deploy → stop → snap atomically
Identity for down is port + networkID handshake, never a PID
file. If port 9720 responds with networkID 200202, that's our node.
If it responds with a different networkID, we refuse to signal — better
to leave a stranger alone than kill the wrong process.
Birth path (against upstream Lux primary)
One-time per env:
CreateChainTxon Lux P-chain (networkID 1/2/3/1337) — registers Zoo EVM blockchain with VMID + genesis blob.AddValidatorTxon Lux P-chain — seeds initial validator set.ConvertNetworkToL1Txon Lux P-chain — flips Zoo to sovereign.
Post-conversion: Zoo's primary network runs fully sovereign on its networkID with its own validator set managed by an on-chain Solidity PoS staking contract. Validators stop talking to upstream Lux at runtime.
Makefile (optional convention)
5 lines, no per-env data. Reads chain.yaml indirectly via lux.
make up env=devnet # ≡ lux up zoo/devnet
make cycle env=mainnet # ≡ lux cycle zoo/mainnet
make cycle-all # cycle every env in sequence
Per-env constants (networkID, ports, genesis path) live in chain.yaml.
No NID_MAIN := 200200 duplication anywhere.
Per-brand DOKS cluster
Every brand owns its own DOKS cluster — no shared k8s across brands.
| Brand | Cluster | Validator clusters |
|---|---|---|
| lux | do-sfo3-lux-k8s | + do-sfo3-lux-test-k8s, -dev-k8s, -bootnode-k8s |
| hanzo | do-sfo3-hanzo-k8s | + do-sfo3-hanzo-dev-k8s |
| zoo | do-sfo3-zoo-k8s | single cluster, 4 envs as namespaces |
| pars | do-sfo3-pars-k8s | single |
| osage | do-sfo3-osage-k8s | single |
| adnexus | do-sfo3-adnexus-k8s | single |
| liquidity | gke_liquidity-{main,test,dev}_us-central1 | GKE per env |
Zoo's k8s namespaces match chain.yaml#networks.<env>.namespace. The
Makefile k8s targets (make k8s-apply env=mainnet) wrap
kubectl --context do-sfo3-zoo-k8s apply -k k8s/zoo-<env>/.
Private universe
~/work/zoo-private/universe/ mirrors this repo with operational
secrets that cannot live in the public universe:
zoo-private/universe/
├── validator-keys/ # KMS pointers for staking signers
├── secrets/ # SOPS-encrypted env blobs
├── bootstrap/ # pre-baked genesis with private allocations
├── snapshots/ # cold backups (RLP + zapdb tarballs)
├── runbooks/ # incident response (private)
└── backup/ # KMS dumps, recovery procedures
Nothing in the public ~/work/zoo/universe/ references KMS paths,
NodeIDs, or proprietary code. Container images are in ghcr.io/zooai/*
only.
chain.yaml — the contract
Schema (typed in pkg/brand/brand.go, parsed once, drives everything):
version: "1"
network:
slug: zoo
type: l1
parent: null
networks:
<env>:
networkID: <uint32> # sovereign-L1: == primaryEvmChainID
primaryEvmChainID: <uint64>
httpPort: <int> # explicit
stakingPort: <int> # explicit, no port+1 rule
genesisFile: <path> # relative to chain.yaml dir
rpcUrl: <url> # remote
cluster: <kubectl-context>
namespace: <k8s-ns>
imageTag: vX.Y.Z
runtime:
dataDirTemplate: "~/.lux/{brand}-{env}"
snapshotDir: "~/work/lux/snapshots"
snapshotNameTemplate: "{networkID}-{brand}-{env}-with-contracts"
serviceLabelTemplate: "ai.lux.{brand}.{env}"
logLevel: info
Validation is enforced on load:
- networkID required, non-zero
- httpPort/stakingPort required, non-zero, distinct, no cross-env collisions
- networkID == primaryEvmChainID when both set (sovereign-L1 rule)
- version == "1" (future bumps will need a migrator)
chain.lock — the runtime invariant
<dataDir>/chain.lock is written on first boot:
{
"version": "1",
"brand": "zoo",
"env": "devnet",
"networkID": 200202,
"httpPort": 9720,
"stakingPort": 9721,
"genesisHash": "0x...",
"createdAt": "2026-06-16T22:00:00Z"
}
Subsequent lux up invocations refuse to mount the dir if the
networkID/httpPort/genesisHash don't match. Prevents silent state
corruption from booting the wrong genesis into an existing dir.
Storage (no Postgres)
Stateful services use Base/SQLite at /app/data/data.db. Litestream
sidecar replicates WAL to MinIO (s3:9000) at 1s sync.
k8s/base/s3.yaml— MinIO StatefulSet, 20Gi DO block storagek8s/base/replicate.yaml— Litestream ConfigMap, S3 endpointk8s/base/bridge.yaml— example consumer (init restore, sidecar replicate, single writer)
Legacy sql.yaml (Postgres) was removed 2026-06-02.
Container registry
ghcr.io/zooai/* only. Banned: registry.digitalocean.com/hanzo/*,
us-docker.pkg.dev/*, mutable tags (:latest, :main, :dev,
:edge, :nightly, :master). Use semver vX.Y.Z or immutable
per-arch branch tags.
Open deployment issues
- zoo-operator
main-amd64— CrashLoopBackOff waits forzooai/operatorCI to publish semver. Bump tovX.Y.Zthen dropmain-amd64across all base/ manifests. - kms-operator + CRDs not installed in
do-sfo3-zoo-k8s. Install from~/work/hanzo/kms, replace manual K8s Secrets with KMSSecret CRs. - Orphan StatefulSets —
mpc-node(pre-T-Chain) andsql(Postgres) lingering. Delete with kubectl. - consul Deployment in zoo-mainnet — not in any manifest.
zoo-chainZooNetwork CR — unreconciled while operator is down.- Genesis for zoo-localnet —
~/work/zoo/genesis/configs/localnet/is empty. Populate to unblocklux up zoo/localnet.
Rules for AI Assistants
- Update THIS file with significant discoveries. Never create new summary files.
- Never commit
.env,.env.local, or any plaintext token. Use KMS atkms.hanzo.ai. - Never push Zoo workloads to any registry other than
ghcr.io/zooai/*. - Never introduce Postgres — SQLite + Litestream is canonical.
- All Zoo apps use the shared hanzo-k8s IAM, but Zoo's issuer is
https://zoolabs.id, neverhanzo.idand neverzoo.id(which has no DNS).id.zoo.networkis an ALIAS of that one issuer, not a second one. Zoo's OAuth clients are declared ininfra/k8s/iam/provision.yamland converged withmake iam-provision— that document is the only supported way to change them; hand-editing an application in a console is silently reverted by the next converge. See the header of that file for the issuer decision and the per-app evidence. - Never reference Lux's C-chain as Zoo's — Zoo has Zoo EVM, sovereign. Lux C-chain is Lux's primary EVM and nothing else uses it.
- Never use "subnet" / "snowman" / "snowball" language — Zoo consensus is Quasar, chains are chains, networks are L1s.
- No shell scripts in this universe. Verbs live in the
luxCLI; the Makefile only delegates.
Zoo mainnet → real 5-validator set (state-preserving zapdb graft) — 2026-07-14
STATUS: ✅ CUT OVER 2026-07-14 (organic, owner-approved). api.zoo.ngo now serves
canonical state from the 5-validator zood-mv set (chainId 200200, tip 13,347,
hash 0x891a539c — verified stable). Cutover = flipped cm zoo-system/zoo-dynamic
routes.yaml service rpc-zoo backend zood-rpc→zood-mv + restarted zoo-ingress.
zood-0 KEPT RUNNING as hot-standby (unrouted, PVC + VolumeSnapshot intact). ZERO
regression — byte-identical state, 729 settlements. Block PRODUCTION on the 5-set
confirms on the next organic/bridge tx (Zoo idle ~22h; no safe funded key to force
one — owner chose organic over a treasury self-send). ROLLBACK if ever needed: flip
rpc-zoo backend back to zood-rpc + restart zoo-ingress (zood-0 still at 13,347).
RETIRE zood-0 (scale sts/zood → 0, KEEP PVC) only AFTER a block >13,347 is observed
on zood-mv. Backup routes.yaml: scratchpad/zoo-routes-backup.yaml.
zood-mv (STS, ns zoo-mainnet) = 5 validators on ghcr.io/luxfi/node:v1.34.9
carrying the canonical Zoo C-Chain (chainId 200200) grafted by direct zapdb
copy from zood-0. NEVER admin_importChain the RLP (diverges @ block 800 on
both v1.34.9 & v1.36.2). Direct coreth-db copy is the faithful path — PROVEN.
Canonical reference (zood-0 == public api.zoo.ngo):
- tip 13347 = 0x891a539c9a82ef872acf608090818c2f08a856f15624e7f2b10f689615f55cc4
- 13346 = 0xdc9b647e… · genesis(0) = 0x7c548af4… · C-blockchainID
23hNkQEfXaGfVVJjDPEPGgvHNfGW5BXWx47kxe67fn1qgL2fsP(sovereign; NOT the old subnet-on-lux4HBM17…innetwork-patch.yaml). - settlement fingerprints: blk808=0x0b8c0b84… 2000=0x210984d2… 3599=0x5e7f7749…
- (task cited
0x3a173563@13346; live public+zood-0 both serve0xdc9b647e@13346→ THAT is canonical;0x3a173563was stale.)
Graft recipe (reproducible): coreth C-db = /data/chainData/network-200200/ <Cbid>/db (50M, nested outer db/+inner db/badgerdb/ w/ ssts) = COMPLETE C
state (proposervm+coreth). Shared /data/db (48K)=P/X only. CSI VolumeSnapshot
data-zood-0 (non-disruptive) → restore PVC → tar -S coreth db → per mv PVC:
rm -rf /data/db /data/chainData + untar coreth db + rm LOCKs + chown 1000.
Fresh 5-val P-Chain rebuilds from zood-mv-genesis (5 stakers, same cChainGenesis
- upgrade.json as zood-0, sha-verified).
PROVEN (all 5): block 13347, tip=0x891a539c(==canonical), gen=0x7c548af4, settlement fingerprints match, P-height 0 / 5 validators, C-bootstrapped, peers=4. Fault-tolerance: killed mv-2 → 4 stayed converged @13347 (quorum 4/5) → mv-2 rejoined @canonical.
BLOCKER (cutover gate): "producing new blocks" unproven — needs a funded tx. Funded accts: treasury 0x9011 (KMS prod secret — DO NOT pull for a test); bridge signer 0xe016 / settler 0x202 (Lux-side, live-nonce → don't break bridge); no faucet deployed. Chain idle ~22h → no organic block in-session.
RESUME (once liveness authorized):
- Prove finalization with a funded op key (owner throwaway, or authorized 0-value self-send from 0x9011 signed in-pod from KMS): 1 tx → all 5 advance to 13348 same hash; kill 1 → 4/5 finalize 13349 → restore → rejoin.
- Cutover:
cm zoo-system/zoo-dynamicroutes.yamlapi-zoo-ngobackendhttp://zood-rpc…:9630→http://zood-mv.zoo-mainnet.svc.cluster.local:9630; restartzoo-ingress(ns zoo-system). Confirm external api.zoo.ngo ≥13348 canonical. - Retire zood-0:
scale sts/zood → 0(KEEPdata-zood-0PVC + snapshot). ALT organic: cutover first, keep zood-0 hot-standby; next bridge round-trip finalizes on 5-set (rollback = flip route back + scale zood-0 up).
BACKUPS (keep until cutover verified): VolumeSnapshot zoo-mainnet/zood0-cchain-snap
(READY, 100Gi, crash-consistent). PVC data-zood-0 intact. NEVER regenesis Zoo.
Zoo crypto-sovereignty: own KMS + MPC + Safes — 2026-07-14
STATUS: ✅ zoo-mpc LIVE (DKG + 3-of-5 sign PROVEN) · ✅ zoo-kms LIVE (auth PROVEN) ·
✅ kms/mpc.zoo.network REPOINTED to the zoo cluster (owner-approved 2026-07-14) · ✅ Zoo
DAO + Z Safes DEPLOYED on 200200. Zoo is fully crypto-sovereign — own MPC ring + own
KMS (Zoo-branded, do-sfo3-zoo-k8s) + own governance Safes. Manifests: k8s/zoo-mpc/,
k8s/zoo-kms/, manifests/safes.yaml.
Secret enumeration (INVESTIGATE — proven, non-destructive)
kms.zoo.network → shared Hanzo KMS (luxfi/kms:1.11.9, ns hanzo, svc kms).
PROVEN zero Zoo secrets stored to migrate → repoint strands nothing:
- native
/v1/kms/orgs/zoo/secrets/*probes (paths ci/bridge/zoo-bridge, env prod+default) → all 404. Token org-scoping calibrated: 403 on orghanzo/zooai, 404 onzoo= authenticated+authorized, genuine not-found (no HTTP list endpoint exists by design). - Infisical
/api/v3NOT implemented on 1.11.9 (SPA fallthrough). Zoo CI (workspacezoo-platform, path/ci: GCP_CREDENTIAL, TOKEN_GITHUB_CICD, DIGITALOCEAN_ACCESS_TOKEN) reads viakms.hanzo.aiand is currently failing at 0s (not populated/consumed). - No zoo KMSSecret CRs, no zoo canonical CRs, no legacy Infisical/casibase store anywhere.
- IAM machine identities (
zoo-{kms,mpc,app,explorer,foundation,mainnet,web3}-iam-creds, ns hanzo) +zoo-*IAM apps exist in the SHARED IAM — unaffected by any repoint.
zoo-mpc — LIVE, DKG PROVEN (5-node, 3-of-5)
ghcr.io/zooai/mpc:1.17.9 (wraps luxfi/mpc:v1.17.9; built via zooai/mpc docker.yml).
All 5 pods "ALL PEERS ARE READY". node-id=zoo-mpc-node-{0..4} and peers MUST use the
nodeID@host:port form — bare host:port yields synthetic peer-N ids that never match
filterSelf(), so self is double-counted (expected=N+1) and /keygen (gated on
ArePeersReady) stalls forever. DKG: POST :9800/keygen {org_id:zoo,wallet_id:zoo-treasury-v1}
→ EVM 0x8756621734fe274fdc426381c9a4f9dec8656243 (ecdsa 502a9a8e…), key persisted
cluster-wide, 3-of-5 threshold sign PROVEN (status:signed). Internal API bearer =
MPC_INTERNAL_API_KEY. Retain PVCs. Stale per-net mpc-node STS (zoo-mainnet/system/
testnet/devnet, all 0/0) are the OLD attempt — LEFT UNTOUCHED (preserve zoo-mpc-secrets
w/ prior signing-key); new ring is the dedicated zoo-mpc ns the bridge tenant targets.
zoo-kms — LIVE, auth PROVEN
kms-0, ns zoo-kms, ghcr.io/zooai/kms:1.11.8 (wraps luxfi/kms:1.11.8 = EXACT
lux-kms-go parity). Secrets-only (ZAP_PORT=0, MPC_VAULT_ID=""). White-labelled:
KMS_IAM_OWNER=zoo → OIDC client_id=zoo-kms owner=zoo (NEVER the luxfi default lux-kms);
IAM issuer hanzo.id. In-cluster health=200, zoo-kms machine login=200. PVC
do-block-storage-retain. Do NOT use luxfi/kms:1.11.10 — it mandates a
consensus-authorizer that fail-closes ("consensus validator authority is empty") without
KMS_CONSENSUS_VALIDATORS/OPERATORS whenever a master key is set → crash-loop. That
upgrade is owned by the KMS-issuer/consensus workstream; the zoo-mpc ring is ready to back
it (MPC_VAULT_ID → zoo-treasury-v1).
Bootstrap (one-time, IMPERATIVE — never commit plaintext; create-if-absent, never regen)
kubectl -n zoo-mpc create secret generic mpc-secrets --from-literal=jwt-secret=$(openssl rand -hex 32) --from-literal=mpc-password=$(openssl rand -hex 32)
kubectl -n zoo-mpc create secret generic mpc-internal-api --from-literal=MPC_INTERNAL_API_KEY=$(openssl rand -hex 32) --from-literal=MPC_THRESHOLD_AUTH_TOKEN=$(openssl rand -hex 32)
kubectl -n zoo-mpc apply -k k8s/zoo-mpc # + copy ghcr-zooai pull secret from zoo-system
kubectl -n zoo-kms create secret generic kms-secrets --from-literal=master-key-b64=$(openssl rand -base64 32) --from-literal=encryption-key-b64=$(openssl rand -base64 32)
kubectl -n zoo-kms create secret generic zoo-kms-oidc --from-literal=client-secret=<zoo-kms IAM clientSecret> --from-literal=state-secret=$(openssl rand -hex 32)
kubectl -n zoo-kms apply -k k8s/zoo-kms
BACK UP encryption-key-b64 + mpc-password (their stores are unreadable without them).
DAO/Z Safes (manifests/safes.yaml) — ✅ DEPLOYED 2026-07-14
Deployed via the PROVEN ~/work/lux/standard/script/deploy_org_safes.sh (default salts,
owner-approved). KEY CORRECTION: 0x9011 was at LIVE nonce 91 on Zoo (NOT nonce 0), so
the earlier nonce-0 predictions (0x5232/0x3Be3) were UNACHIEVABLE (0x413e…factory =
CREATE(0x9011,1), unreachable). The tool adapts to the live nonce → the ACTUAL deployed set:
- DAO Safe 0x229599f227231d8C90fcF1a78589F5DC4b7A6962 (salt zoo.dao.safe.v1) — 1/1 owner 0x9011 ✓
- Z/Team Safe 0xb8BbCdde5E8713f9849D1520D71Bc08Af94E6F00 (salt zoo.team.safe.v1) — 1/1 owner 0x9011 ✓
- Foundation Timelock 0x31C89A0509F7783b742B01Be9103c05BbF4B9510 — minDelay 86400, DAO=proposer ✓
- infra: singleton 0xF034942c / factory 0x2cd30691 / handler 0xcEfF061c / multisend 0x4Dc0A7aC (nonce 91-94)
- Block-production PROOF: the 7 deploy txs were the Zoo 5-set's first blocks — tip 13347→13354,
all 5 zood-mv converged (13348 hash 0xf50dcdf3…, 13353 hash 0xe90cee14…). Record:
~/work/lux/standard/deployments/org-safes/zoo-200200.json. - Salts are chain-specific (nonce-based); for cross-org-uniform addresses use Arachnid (future).
Repoint — ✅ DONE + VERIFIED 2026-07-14 (owner-approved)
kms.zoo.network + mpc.zoo.network (+mpc-api) cut over from the Hanzo cluster to the ZOO
cluster. The zoo cluster routes via the Traefik file provider (cm zoo-system/zoo-dynamic
routes.yaml — NOT k8s Ingress; class-annotation Ingresses are NOT picked up), TLS via Traefik's
letsencrypt DNS-01 Cloudflare resolver. Cutover done:
- added
kms-zoo/mpc-zoorouters+services tozoo-dynamic(backendkms.zoo-kms.svc:80/mpc-api-svc.zoo-mpc.svc:8081, passHostHeader for KMS white-labelling); - flipped the 3 Cloudflare A records → zoo LB
209.38.118.103(grey-cloud; via thecloudflare-dns01-keyGlobal API Key, X-Auth-Email [email protected]); - removed
kms.zoo.networkfrom thehanzo-domainsCR + deleted the generated ingresses +hanzo-mpc/mpc-zoo-ingress. VERIFIED public:kms.zoo.network/v1/kms/health200 + zoo-kms login 200 (Zoo cluster's own KMS). REVERSIBLE: re-add thehanzo-domainskms.zoo.network entry + flip DNS back. Zero secrets lost (proven empty). Backup:scratchpad/zoo-dynamic-BACKUP.yaml(this session). NOTE: Zoo CI (ci.yml) readskms.hanzo.ai(defaultKMS_URL), NOT kms.zoo.network — UNAFFECTED by the repoint; its Infisical/api/v3fetch was already non-functional (unimplemented on luxfi/kms + CI failing at 0s). Repointing CI to kms.zoo.network is a FUTURE step (populate the Zoo KMS with thezoo-platform/cisecrets + switch CI to the native/v1/kmsAPI).
Lux KMS reality (step 6 answer)
lux-kms-go/kms-0 (v1.11.8) runs SECRETS-ONLY: MPC_VAULT_ID="" + ZAP_PORT=0 — it
does NOT do MPC-backed signing; lux-mpc (5 nodes, healthy) exists but is NOT wired to
the KMS. Zoo is deployed at exact parity (KMS secrets-only + independent, proven MPC ring).
Last updated 2026-07-14 — Zoo crypto-sovereignty COMPLETE: own KMS+MPC LIVE/proven, DAO+Z Safes DEPLOYED (200200), kms/mpc.zoo.network REPOINTED to zoo cluster. Prior: 5-validator graft; 2026-06-16 Sovereign L1 + lux CLI.