fix(nodes): correct luxd deploy to canonical Lux

- image luxfi/luxd:latest -> ghcr.io/luxfi/node:latest (the pullable one)
- numeric network-id (localnet=1337/devnet=3/testnet=2/mainnet=1), drop
  Avalanche 'fuji' and 'subnet'/--track-subnets language
- HTTP 9630 / staking 9631 (not 9650); --data-dir=/data
- stage bundled VM plugins into --plugin-dir so C-Chain EVM + zk/quantum/
  threshold/bridge/dex VMs load (mirrors luxd-boot.sh)
- fix port allocator feedback loop that overflowed host ports past 65535
This commit is contained in:
2026-06-19 00:32:57 +00:00
parent d6203fd19b
commit 9090b70ea2
+62 -42
View File
@@ -69,18 +69,20 @@ CHAIN_IMAGES = {
"blast": {
"execution": {"blast": "blastio/blast-geth:latest"},
},
# ============ HANZO ECOSYSTEM (Lux-based) ============
# ============ LUX ECOSYSTEM (luxd — Lux, Zoo, Pars, Hanzo) ============
# All four run the same luxd node (ghcr.io/luxfi/node). Only the primary
# network-id and genesis differ; Zoo/Pars/Hanzo are Lux-based sovereign L1s.
"lux": {
"execution": {"luxd": "luxfi/luxd:latest"},
"execution": {"luxd": "ghcr.io/luxfi/node:latest"},
},
"zoo": {
"execution": {"luxd": "luxfi/luxd:latest"}, # Zoo subnet on Lux
"execution": {"luxd": "ghcr.io/luxfi/node:latest"}, # Zoo: Lux-based sovereign L1
},
"pars": {
"execution": {"luxd": "luxfi/luxd:latest"}, # Pars subnet on Lux
"execution": {"luxd": "ghcr.io/luxfi/node:latest"}, # Pars: Lux-based sovereign L1
},
"hanzo": {
"execution": {"luxd": "luxfi/luxd:latest"}, # Hanzo subnet on Lux
"execution": {"luxd": "ghcr.io/luxfi/node:latest"}, # Hanzo: Lux-based sovereign L1
},
# ============ MAJOR L1s ============
"bitcoin": {
@@ -240,10 +242,10 @@ CHAIN_PORTS = {
"mantle": {"rpc": 8545, "ws": 8546},
"blast": {"rpc": 8545, "ws": 8546},
# Hanzo ecosystem
"lux": {"rpc": 9650, "ws": 9650, "p2p": 9651},
"zoo": {"rpc": 9650, "ws": 9650, "p2p": 9651},
"pars": {"rpc": 9650, "ws": 9650, "p2p": 9651},
"hanzo": {"rpc": 9650, "ws": 9650, "p2p": 9651},
"lux": {"rpc": 9630, "ws": 9630, "p2p": 9631},
"zoo": {"rpc": 9630, "ws": 9630, "p2p": 9631},
"pars": {"rpc": 9630, "ws": 9630, "p2p": 9631},
"hanzo": {"rpc": 9630, "ws": 9630, "p2p": 9631},
# Major L1s
"bitcoin": {"rpc": 8332, "p2p": 8333},
"solana": {"rpc": 8899, "ws": 8900, "p2p": 8001},
@@ -374,7 +376,12 @@ def allocate_ports() -> Dict[str, int]:
if bindings:
for binding in bindings:
host_port = int(binding.get("HostPort", 0))
if host_port >= _port_counter["rpc"]:
# Only seed the counter from the RPC/ws range.
# High p2p/beacon ports (30303+, 19000+) must not
# drive the counter up — otherwise each node's p2p
# port feeds back and inflates the next allocation
# until it overflows 65535.
if _port_counter["rpc"] <= host_port < 30000:
_port_counter["rpc"] = host_port + 10
_port_counter["ws"] = host_port + 11
except Exception:
@@ -820,10 +827,10 @@ async def start_node_container(node_id: str, node: dict):
_nodes[node_id]["container_id"] = exec_container.id[:12]
_nodes[node_id]["containers"] = [c.id[:12] for c in containers_created]
# ============ LUX ECOSYSTEM (Lux, Zoo, Pars, Hanzo) ============
# ============ LUX ECOSYSTEM (luxd — Lux, Zoo, Pars, Hanzo) ============
elif chain in ["lux", "zoo", "pars", "hanzo"]:
exec_images = chain_images.get("execution", {})
image = exec_images.get("luxd", "luxfi/luxd:latest")
image = exec_images.get("luxd", "ghcr.io/luxfi/node:latest")
try:
client.images.get(image)
@@ -835,47 +842,60 @@ async def start_node_container(node_id: str, node: dict):
try:
old = client.containers.get(container_name)
old.remove(force=True)
except:
except docker.errors.NotFound:
pass
# Configure luxd based on network
lux_network_id = "mainnet" if network == "mainnet" else "fuji" if network == "testnet" else "local"
# Canonical Lux primary network-id (matches luxd-boot.sh):
# localnet=1337, devnet=3, testnet=2, mainnet=1. Zoo/Pars/Hanzo are
# Lux-based sovereign L1s — same luxd boot; genesis selects the chain.
net_id = {"local": 1337, "localnet": 1337, "devnet": 3,
"testnet": 2, "mainnet": 1}.get(network, 1337)
# For local development, use special config
if network == "local":
lux_cmd = [
"--network-id=local",
"--http-host=0.0.0.0",
"--http-port=9650",
"--staking-enabled=false",
"--db-type=memdb",
# luxd serves C-Chain HTTP on 9630, staking on 9631 (not 9650).
# Custom VM plugins load from --plugin-dir (mount .so into /data/plugins).
lux_cmd = [
f"--network-id={net_id}",
"--http-host=0.0.0.0",
"--http-port=9630",
"--staking-port=9631",
"--data-dir=/data",
"--plugin-dir=/data/plugins",
]
# Single-node local/dev: run without a peer quorum. Real staking keys
# under /data/staking are picked up automatically when present (prod).
if network in ("local", "localnet", "devnet"):
lux_cmd += [
"--sybil-protection-enabled=false",
"--network-health-min-conn-peers=0",
"--skip-bootstrap=true",
]
else:
lux_cmd = [
f"--network-id={lux_network_id}",
"--http-host=0.0.0.0",
"--http-port=9650",
"--public-ip-resolution-service=opendns",
]
# Add subnet-specific flags for Zoo/Pars/Hanzo
if chain in ["zoo", "pars", "hanzo"]:
# These would have subnet-specific configs
lux_cmd.extend([
"--track-subnets=", # Add actual subnet IDs for production
])
lux_cmd.append("--public-ip-resolution-service=opendns")
# luxd loads VMs from --plugin-dir. The C-Chain EVM ships as a plugin
# (.so named by vmID under /luxd/build/plugins); custom VM plugins drop
# in the same dir. Stage the bundled plugins into the data plugin dir,
# then exec luxd — same shape as the canonical luxd-boot.sh. The image
# Cmd is ["./luxd"] with no Entrypoint, so we wrap in sh -c.
luxd_argv = " ".join(lux_cmd)
stage = "mkdir -p /data/plugins && cp -n /luxd/build/plugins/* /data/plugins/ 2>/dev/null; "
# Publish RPC always; publish staking only if it maps to a valid host
# port (guard against an out-of-range allocation).
lux_port_map = {"9630/tcp": ports["rpc"]}
staking_host = ports.get("p2p") or (ports["rpc"] + 1)
if 1024 < staking_host <= 65535:
lux_port_map["9631/tcp"] = staking_host
container = client.containers.run(
image,
command=lux_cmd,
entrypoint=["/bin/sh", "-c"],
# Single-list element: the whole script is one argv to `sh -c`
# (a bare string would be word-split by docker-py).
command=[f"{stage}exec /luxd/build/luxd {luxd_argv}"],
name=container_name,
detach=True,
ports={
"9650/tcp": ports["rpc"],
"9651/tcp": ports.get("p2p", 9651 + (ports["rpc"] - 9650)),
},
ports=lux_port_map,
volumes={
f"bootnode-data-{node_id[:8]}": {"bind": "/root/.luxd", "mode": "rw"},
f"bootnode-data-{node_id[:8]}": {"bind": "/data", "mode": "rw"},
},
network=network_name,
restart_policy={"Name": "unless-stopped"},