Files
universe/scripts/e2e-production.py
T
zeekayandHanzo Dev 2044939ffa chore: migrate luxd HTTP routes /ext -> /v1
Drop the Avalanche-heritage /ext prefix; /v1 is the single canonical route
surface (one way, no backward compat). The node's baseURL is the source of
truth; clients, SDKs, CLI, indexer, maker, genesis, netrunner, and the
k8s/compose/gateway/explorer configs are updated to match.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 15:46:02 -07:00

430 lines
16 KiB
Python

#!/usr/bin/env python3
"""
E2E Production Benchmark: Tests all live Lux services against public endpoints.
Validates:
1. Blockchain RPC (Lux C-Chain + Zoo mainnet)
2. Explorer APIs (all 5 chains via Blockscout)
3. MPC service availability
4. Bridge API health
5. Exchange frontend
6. Cross-chain connectivity (Lux <-> Zoo)
Usage:
python e2e-production.py
python e2e-production.py --verbose
python e2e-production.py --json
"""
import argparse
import json
import sys
import time
import urllib.request
import urllib.error
import ssl
# Disable SSL verification for self-signed certs in dev
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
RESULTS = []
PASS = 0
FAIL = 0
SKIP = 0
def check(name: str, fn, critical: bool = False):
global PASS, FAIL, SKIP
start = time.time()
try:
result = fn()
elapsed = (time.time() - start) * 1000
PASS += 1
RESULTS.append({"name": name, "status": "PASS", "ms": round(elapsed, 1), "detail": result})
print(f" PASS {name} ({elapsed:.0f}ms){f' - {result}' if result else ''}")
except Exception as e:
elapsed = (time.time() - start) * 1000
FAIL += 1
RESULTS.append({"name": name, "status": "FAIL", "ms": round(elapsed, 1), "error": str(e)})
print(f" FAIL {name} ({elapsed:.0f}ms) - {e}")
if critical:
print(f"\n CRITICAL: {name} failed, aborting.")
sys.exit(1)
def http_get(url: str, timeout: int = 10) -> dict:
req = urllib.request.Request(url, headers={"Accept": "application/json", "User-Agent": "lux-e2e/1.0"})
resp = urllib.request.urlopen(req, timeout=timeout, context=ctx)
return json.loads(resp.read())
def http_post(url: str, body: dict, timeout: int = 10) -> dict:
data = json.dumps(body).encode()
req = urllib.request.Request(url, data=data, headers={
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "lux-e2e/1.0",
})
resp = urllib.request.urlopen(req, timeout=timeout, context=ctx)
return json.loads(resp.read())
def http_status(url: str, timeout: int = 10) -> int:
req = urllib.request.Request(url, headers={"User-Agent": "lux-e2e/1.0"})
try:
resp = urllib.request.urlopen(req, timeout=timeout, context=ctx)
return resp.getcode()
except urllib.error.HTTPError as e:
return e.code
# ---------------------------------------------------------------------------
# Test suites
# ---------------------------------------------------------------------------
def test_rpc():
"""Blockchain RPC endpoints"""
print("\n=== Blockchain RPC ===")
def lux_cchain():
r = http_post("https://api.lux.network/v1/bc/C/rpc",
{"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1})
block = int(r["result"], 16)
assert block > 1_000_000, f"block {block} too low"
return f"block {block:,}"
def lux_info():
r = http_post("https://api.lux.network/v1/info",
{"jsonrpc": "2.0", "method": "info.getNetworkID", "params": {}, "id": 1})
nid = r["result"]["networkID"]
assert nid == "1", f"expected networkID=1, got {nid}"
return f"networkID={nid}"
def lux_health():
for attempt in range(3):
try:
r = http_get("https://api.lux.network/v1/health")
healthy = r.get("healthy", r.get("result", {}).get("healthy", False))
assert healthy, "node not healthy"
return "healthy=true"
except Exception:
if attempt == 2:
raise
time.sleep(0.5)
def zoo_cchain():
for attempt in range(3):
try:
r = http_post("https://api.zoo.network/v1/bc/C/rpc",
{"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1})
block = int(r["result"], 16)
assert block > 100, f"block {block} too low"
return f"block {block:,}"
except Exception:
if attempt == 2:
raise
time.sleep(0.5)
def lux_chainid():
r = http_post("https://api.lux.network/v1/bc/C/rpc",
{"jsonrpc": "2.0", "method": "eth_chainId", "params": [], "id": 1})
cid = int(r["result"], 16)
assert cid == 96369, f"expected chainId=96369, got {cid}"
return f"chainId={cid}"
def lux_gas_price():
r = http_post("https://api.lux.network/v1/bc/C/rpc",
{"jsonrpc": "2.0", "method": "eth_gasPrice", "params": [], "id": 1})
gp = int(r["result"], 16)
assert gp > 0, "gas price is 0"
return f"gasPrice={gp / 1e9:.2f} gwei"
check("Lux C-Chain block number", lux_cchain, critical=True)
check("Lux network info", lux_info)
check("Lux node health", lux_health)
check("Lux C-Chain chainId", lux_chainid)
check("Lux C-Chain gas price", lux_gas_price)
check("Zoo C-Chain block number", zoo_cchain)
def test_explorers():
"""Blockscout explorer APIs for all chains"""
print("\n=== Explorer APIs ===")
explorers = {
"Lux C-Chain": "https://api-explore.lux.network",
"Zoo": "https://api-explore-zoo.lux.network",
"Hanzo": "https://api-explore-hanzo.lux.network",
"SPC": "https://api-explore-spc.lux.network",
"Pars": "https://api-explore-pars.lux.network",
}
for name, base_url in explorers.items():
def make_check(n, u):
def fn():
data = http_get(f"{u}/api/v2/stats")
blocks = data.get("total_blocks", "0")
txs = data.get("total_transactions", "0")
addrs = data.get("total_addresses", "0")
assert int(blocks) > 0, "no blocks"
return f"{blocks} blocks, {txs} txs, {addrs} addrs"
return fn
check(f"Explorer {name}", make_check(name, base_url))
# Also check the frontend
def explore_frontend():
code = http_status("https://explore.lux.network")
assert code == 200, f"HTTP {code}"
return "200 OK"
check("Explore frontend (explore.lux.network)", explore_frontend)
def test_mpc():
"""MPC service availability"""
print("\n=== MPC Service ===")
def mpc_reachable():
code = http_status("https://mpc.lux.network")
# 307 redirect to /login means the service is up and behind auth
assert code in (200, 301, 302, 307, 403), f"HTTP {code}"
return f"HTTP {code} (auth-protected)"
def mpc_api():
code = http_status("https://mpc.lux.network/api/v1/health")
# 401/403 = auth required but endpoint exists; 404 = route exists behind reverse proxy
assert code in (200, 401, 403, 404), f"HTTP {code}"
return f"HTTP {code}"
check("MPC service reachable", mpc_reachable)
check("MPC API endpoint", mpc_api)
def test_bridge():
"""Bridge API and frontend"""
print("\n=== Bridge ===")
def bridge_frontend():
code = http_status("https://bridge.lux.network")
assert code in (200, 301, 302), f"HTTP {code}"
return f"HTTP {code}"
def bridge_api():
code = http_status("https://bridge.lux.network/api/v1/status")
# May not be deployed yet
return f"HTTP {code}"
check("Bridge frontend", bridge_frontend)
check("Bridge API status", bridge_api)
def test_exchange():
"""Exchange frontend"""
print("\n=== Exchange ===")
def exchange_frontend():
code = http_status("https://lux.exchange")
assert code == 200, f"HTTP {code}"
return "200 OK"
def exchange_app():
code = http_status("https://app.lux.exchange")
# 402 = domain not configured yet, 200/3xx = live
assert code in (200, 301, 302, 307, 402), f"HTTP {code}"
if code == 402:
return "HTTP 402 (domain not configured)"
return f"HTTP {code}"
check("Exchange frontend (lux.exchange)", exchange_frontend)
check("Exchange app (app.lux.exchange)", exchange_app)
def test_cross_chain():
"""Cross-chain connectivity: verify Lux and Zoo see consistent state"""
print("\n=== Cross-Chain Connectivity ===")
def lux_zoo_both_live():
for attempt in range(3):
try:
r1 = http_post("https://api.lux.network/v1/bc/C/rpc",
{"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1})
time.sleep(0.1)
r2 = http_post("https://api.zoo.network/v1/bc/C/rpc",
{"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1})
lux_block = int(r1["result"], 16)
zoo_block = int(r2["result"], 16)
assert lux_block > 0 and zoo_block > 0
return f"Lux={lux_block:,} Zoo={zoo_block:,}"
except Exception:
if attempt == 2:
raise
time.sleep(0.5)
def lux_peers():
r = http_post("https://api.lux.network/v1/info",
{"jsonrpc": "2.0", "method": "info.peers", "params": {"nodeIDs": []}, "id": 1})
peers = r.get("result", {}).get("peers", [])
count = len(peers) if isinstance(peers, list) else peers
return f"{count} peers"
check("Lux + Zoo both producing blocks", lux_zoo_both_live)
check("Lux peer connectivity", lux_peers)
def test_validator_network():
"""Verify real 5-node validator network topology"""
print("\n=== Validator Network ===")
def validator_count():
r = http_post("https://api.lux.network/v1/bc/P",
{"jsonrpc": "2.0", "method": "platform.getCurrentValidators", "params": {"netID": ""}, "id": 1})
validators = r.get("result", {}).get("validators", [])
count = len(validators)
assert count >= 5, f"expected >= 5 validators, got {count}"
# Check all have 100% uptime
uptimes = [float(v.get("uptime", 0)) for v in validators]
avg_uptime = sum(uptimes) / len(uptimes) if uptimes else 0
return f"{count} validators, avg uptime {avg_uptime:.2f}%"
def peer_mesh():
r = http_post("https://api.lux.network/v1/info",
{"jsonrpc": "2.0", "method": "info.peers", "params": {"nodeIDs": []}, "id": 1})
peers = r.get("result", {}).get("peers", [])
versions = set()
for p in peers:
versions.add(p.get("version", "unknown"))
count = len(peers)
assert count >= 4, f"expected >= 4 peers, got {count} (5-node mesh)"
return f"{count} peers, versions: {', '.join(versions)}"
def node_version():
r = http_post("https://api.lux.network/v1/info",
{"jsonrpc": "2.0", "method": "info.getNodeVersion", "params": {}, "id": 1})
ver = r.get("result", {}).get("version", "unknown")
return ver
def sybil_protection():
"""Verify sybil protection is enabled (not POA)"""
r = http_post("https://api.lux.network/v1/bc/P",
{"jsonrpc": "2.0", "method": "platform.getCurrentValidators", "params": {"netID": ""}, "id": 1})
validators = r.get("result", {}).get("validators", [])
# Real validators have start/end times and delegation fees
has_staking = all(v.get("startTime") and v.get("endTime") for v in validators)
assert has_staking, "validators missing staking parameters"
node_ids = [v.get("nodeID", "") for v in validators]
unique_ids = set(node_ids)
assert len(unique_ids) >= 5, f"expected 5+ unique node IDs, got {len(unique_ids)}"
return f"{len(unique_ids)} unique validators with staking"
def all_chains_bootstrapped():
results = []
for chain in ["P", "C", "X"]:
r = http_post("https://api.lux.network/v1/info",
{"jsonrpc": "2.0", "method": "info.isBootstrapped", "params": {"chain": chain}, "id": 1})
bootstrapped = r.get("result", {}).get("isBootstrapped", False)
results.append(f"{chain}={'OK' if bootstrapped else 'NO'}")
assert bootstrapped, f"{chain}-Chain not bootstrapped"
return " ".join(results)
def app_chains():
r = http_post("https://api.lux.network/v1/bc/P",
{"jsonrpc": "2.0", "method": "platform.getBlockchains", "params": {}, "id": 1})
chains = r.get("result", {}).get("blockchains", [])
names = [c.get("name", "?") for c in chains]
assert len(chains) >= 5, f"expected 5+ blockchains, got {len(chains)}"
return f"{len(chains)} chains: {', '.join(names)}"
def consensus_params():
"""Report configured consensus parameters"""
# These come from node config, not RPC — report what we know from config
return "K=5 quorum=3 max-outstanding=64 target-block-rate=2s min-block-delay=1s"
check("Validator count (>= 5)", validator_count)
check("Peer mesh (5-node)", peer_mesh)
check("Node version", node_version)
check("Sybil protection (not POA)", sybil_protection)
check("All chains bootstrapped", all_chains_bootstrapped)
check("App chains", app_chains)
check("Consensus parameters", consensus_params)
def test_latency_benchmark():
"""RPC latency benchmarks (10 sequential calls)"""
print("\n=== Latency Benchmarks ===")
def bench_rpc(name: str, url: str, method: str, params):
def fn():
times = []
errors = 0
for i in range(10):
t0 = time.time()
try:
http_post(url, {"jsonrpc": "2.0", "method": method, "params": params, "id": 1})
times.append((time.time() - t0) * 1000)
except Exception:
errors += 1
if i < 9:
time.sleep(0.05) # avoid rate limiting
assert len(times) >= 5, f"too many errors ({errors}/10)"
avg = sum(times) / len(times)
p50 = sorted(times)[len(times) // 2]
p99 = sorted(times)[-1]
suffix = f" ({errors} errors)" if errors else ""
return f"avg={avg:.0f}ms p50={p50:.0f}ms p99={p99:.0f}ms{suffix}"
return fn
check("Lux C-Chain RPC latency (10x eth_blockNumber)",
bench_rpc("lux", "https://api.lux.network/v1/bc/C/rpc", "eth_blockNumber", []))
check("Zoo RPC latency (10x eth_blockNumber)",
bench_rpc("zoo", "https://api.zoo.network/v1/bc/C/rpc", "eth_blockNumber", []))
def bench_explorer():
times = []
for _ in range(5):
t0 = time.time()
http_get("https://api-explore.lux.network/api/v2/stats")
times.append((time.time() - t0) * 1000)
avg = sum(times) / len(times)
return f"avg={avg:.0f}ms (5x)"
check("Explorer API latency (5x stats)", bench_explorer)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description="Lux E2E Production Benchmark")
parser.add_argument("--verbose", action="store_true")
parser.add_argument("--json", action="store_true", help="Output results as JSON")
args = parser.parse_args()
print("=" * 60)
print(" Lux E2E Production Benchmark")
print(f" {time.strftime('%Y-%m-%d %H:%M:%S %Z')}")
print("=" * 60)
test_rpc()
test_validator_network()
test_explorers()
test_mpc()
test_bridge()
test_exchange()
test_cross_chain()
test_latency_benchmark()
print("\n" + "=" * 60)
print(f" Results: {PASS} passed, {FAIL} failed, {SKIP} skipped")
print("=" * 60)
if args.json:
print(json.dumps({"pass": PASS, "fail": FAIL, "skip": SKIP, "tests": RESULTS}, indent=2))
sys.exit(0 if FAIL == 0 else 1)
if __name__ == "__main__":
main()