An RPC that answers eth_chainId says nothing about whether the chain still makes blocks, and the status page could not tell a halted chain from a live one. chain-tip answers /v1/tip/<chain> with height, timestamp and age_seconds for lux-mainnet and lux-testnet, read from their luxd-archive Services. The chains are fixed in its env; a request names a chain, never a URL. ClusterIP only. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
121 lines
4.5 KiB
YAML
121 lines
4.5 KiB
YAML
# How old each Lux chain's newest block is, for the status pages.
|
|
#
|
|
# An RPC that answers eth_chainId says nothing about whether the chain still
|
|
# makes blocks: an archive with no peers answers every read. The status checks
|
|
# (hanzoai/universe charts/app/values/hanzo/status-lux.yaml, status-lux-config)
|
|
# read /v1/tip/<chain> here and hold age_seconds to a limit, so a chain that has
|
|
# stopped shows as down.
|
|
#
|
|
# The chain list is fixed in CHAINS; a request names a chain, never a URL. The
|
|
# only call made is eth_getBlockByNumber("latest"). ClusterIP only: nothing
|
|
# outside the cluster reaches this Service.
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata: {name: chain-tip-code, namespace: lux-mainnet}
|
|
data:
|
|
tip.py: |
|
|
import json, os, time, urllib.request
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
CHAINS = json.loads(os.environ["CHAINS"])
|
|
TIMEOUT = float(os.environ.get("TIMEOUT_SECONDS", "5"))
|
|
CALL = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "eth_getBlockByNumber",
|
|
"params": ["latest", False]}).encode()
|
|
|
|
def tip(url):
|
|
req = urllib.request.Request(url, CALL, {"content-type": "application/json", "user-agent": "lux-chain-tip"})
|
|
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
|
block = json.load(r)["result"]
|
|
return int(block["number"], 16), int(block["timestamp"], 16)
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
if self.path == "/healthz":
|
|
return self.reply(200, {"status": "ok"})
|
|
prefix = "/v1/tip/"
|
|
name = self.path[len(prefix):] if self.path.startswith(prefix) else ""
|
|
if name not in CHAINS:
|
|
return self.reply(404, {"error": "unknown chain", "chains": sorted(CHAINS)})
|
|
try:
|
|
height, stamp = tip(CHAINS[name])
|
|
except Exception as e:
|
|
return self.reply(502, {"chain": name, "error": type(e).__name__})
|
|
self.reply(200, {"chain": name, "height": height, "timestamp": stamp,
|
|
"age_seconds": int(time.time()) - stamp})
|
|
|
|
def reply(self, code, body):
|
|
data = json.dumps(body).encode()
|
|
self.send_response(code)
|
|
self.send_header("content-type", "application/json")
|
|
self.send_header("content-length", str(len(data)))
|
|
self.end_headers()
|
|
self.wfile.write(data)
|
|
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
ThreadingHTTPServer(("", 8080), Handler).serve_forever()
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: chain-tip
|
|
namespace: lux-mainnet
|
|
labels: {app.kubernetes.io/name: chain-tip, app.kubernetes.io/part-of: lux}
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels: {app.kubernetes.io/name: chain-tip}
|
|
template:
|
|
metadata:
|
|
labels: {app.kubernetes.io/name: chain-tip, app.kubernetes.io/part-of: lux}
|
|
spec:
|
|
automountServiceAccountToken: false
|
|
enableServiceLinks: false
|
|
nodeSelector:
|
|
kubernetes.io/arch: amd64
|
|
hanzo.ai/role: apps
|
|
securityContext:
|
|
runAsUser: 65532
|
|
runAsGroup: 65532
|
|
runAsNonRoot: true
|
|
seccompProfile: {type: RuntimeDefault}
|
|
containers:
|
|
- name: tip
|
|
image: python:3.12-alpine@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df
|
|
command: [python, -u, /app/tip.py]
|
|
env:
|
|
- name: CHAINS
|
|
value: '{"lux-mainnet":"http://luxd-archive.lux-mainnet.svc.cluster.local:9630/v1/chain/C/rpc","lux-testnet":"http://luxd-archive.lux-testnet.svc.cluster.local:9630/v1/chain/C/rpc"}'
|
|
ports:
|
|
- {name: http, containerPort: 8080}
|
|
readinessProbe:
|
|
httpGet: {path: /healthz, port: http}
|
|
periodSeconds: 10
|
|
livenessProbe:
|
|
httpGet: {path: /healthz, port: http}
|
|
periodSeconds: 30
|
|
resources:
|
|
requests: {cpu: 5m, memory: 24Mi}
|
|
limits: {cpu: 100m, memory: 64Mi}
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
capabilities: {drop: [ALL]}
|
|
volumeMounts:
|
|
- {name: code, mountPath: /app, readOnly: true}
|
|
volumes:
|
|
- name: code
|
|
configMap: {name: chain-tip-code}
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: chain-tip
|
|
namespace: lux-mainnet
|
|
labels: {app.kubernetes.io/name: chain-tip, app.kubernetes.io/part-of: lux}
|
|
spec:
|
|
selector: {app.kubernetes.io/name: chain-tip}
|
|
ports:
|
|
- {name: http, port: 80, targetPort: http}
|