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]>
430 lines
16 KiB
Python
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()
|