49 lines
5.7 KiB
Python
49 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""The Devnet 2 standing gate's reads without the install step (6 October 2026, 21:4xZ; the seed moved to igneum-build-1,
|
|
user build, rpc 27610, so devnet2-gate.sh's root@ ssh and standard ports no longer fit the seed). Reads every Devnet 2 box
|
|
(registry rows with devnet2 or dn2_seed; the seed through its seed_env ports) and prints one line: PASS only with zero
|
|
rejected blocks on every box since SINCE, no selected-chain reorg over depth 3, equal exec state roots at a common height,
|
|
at least one paid segment record, every node on one version. Usage: dn2-check.py [--since 2026-10-06T21:00:00Z]"""
|
|
import sys, os, json, time, datetime
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))); from lib import Box, Registry
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
def now(): return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
since = sys.argv[sys.argv.index("--since") + 1] if "--since" in sys.argv else (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=30)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
since_log = since.replace("T", " ").rstrip("Z")
|
|
# a box's log may stamp local time (igneum-build-1 logs +02:00): shift the since stamp by the offset the log's lines carry
|
|
SINCE_SHIFT = r"""off=$(grep -oE '^[0-9-]+ [0-9:.]+[+-][0-9]{2}:[0-9]{2}' LOGFILE | tail -1 | grep -oE '[+-][0-9]{2}:[0-9]{2}$'); s='SINCE_UTC'; if [ -n "$off" ] && [ "$off" != "+00:00" ]; then sign=${off:0:1}; h=$((10#${off:1:2})); m=$((10#${off:4:2})); secs=$(( (h*3600+m*60) )); [ "$sign" = "-" ] && secs=$((-secs)); s=$(date -u -d "$s UTC $secs seconds" +'%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$s"); fi; echo "$s" """
|
|
reg = Registry.load()
|
|
rows = [(i, b) for i, b in reg.items() if b.get("state") != "destroyed" and b.get("ssh_host") and (b.get("devnet2") or b.get("dn2_seed"))]
|
|
def read(item):
|
|
i, b = item; env = b.get("seed_env") or {}
|
|
log = env.get("SEED_LOG", "/root/fleet/dn2-node.log"); rpc = env.get("SEED_RPC", "27610" if b["label"].startswith("wave") else "26610"); evm = env.get("SEED_EVM", "27790" if b["label"].startswith("wave") else "26790"); miner = env.get("SEED_MINER", "/opt/igneum/pkg/bin/igneum-miner")
|
|
if b["label"] == "dn2-seed": rpc, evm = "27610", "27790" # its 26610 is the rehearsal box's live node; the Devnet 2 node sits on the alternate ports
|
|
box = Box(b["ssh_host"], b["ssh_port"], b["label"], i, b.get("wallet"), b.get("provider"), b.get("ssh_user", "root"))
|
|
shift = SINCE_SHIFT.replace("LOGFILE", log).replace("SINCE_UTC", since_log)
|
|
cmd = f"""w=$({miner} watch 1 grpc://127.0.0.1:{rpc} 2>/dev/null | grep -o 'blocks=[0-9]*.*synced=[a-z]*' | tail -1); echo W $w
|
|
SL="$({shift})"; echo SINCE_LOCAL $SL
|
|
echo REJ $(awk -v s="$SL" '($1" "$2) >= s' {log} | grep -c 'PoW rejected') REORG $(awk -v s="$SL" '($1" "$2) >= s' {log} | grep -oE 'selected-chain reorg: [0-9]+ chain blocks' | grep -oE '[0-9]+ chain' | awk 'BEGIN{{m=0}} {{if ($1>m) m=$1}} END{{print m}}')
|
|
echo EXEC $(curl -s -m 6 -X POST -H 'Content-Type: application/json' --data '{{"jsonrpc":"2.0","id":1,"method":"igneum_getExecStatus","params":[]}}' http://127.0.0.1:{evm}/ | grep -oE '"executedTip":"0x[0-9a-f]+"' | grep -oE '0x[0-9a-f]+')
|
|
echo PAID $(curl -s -m 6 -X POST -H 'Content-Type: application/json' --data '{{"jsonrpc":"2.0","id":1,"method":"igneum_getProvingStatus","params":[]}}' http://127.0.0.1:{evm}/ | grep -oE '"paidSegments":[0-9]+' | grep -oE '[0-9]+')
|
|
echo VER $(grep -oE 'igneumd/2\\.1\\.0-[0-9a-f]+' {log} | tail -1) DIGEST $(grep -o 'digest: [0-9a-f]*' {log} | tail -1 | awk '{{print substr($2,1,16)}}')"""
|
|
rc, out, err = box.run(cmd, 60); d = {"label": b["label"], "seed": bool(b.get("dn2_seed")), "evm": evm, "box": box}
|
|
for ln in out.splitlines():
|
|
p = ln.split()
|
|
if not p: continue
|
|
if p[0] == "W": d.update({k: v for k, v in (kv.split("=", 1) for kv in p[1:] if "=" in kv)})
|
|
elif p[0] == "REJ" and len(p) >= 4: d["rej"] = int(p[1] or 0); d["reorg"] = int(p[3] or 0)
|
|
elif p[0] == "EXEC" and len(p) > 1: d["exec"] = int(p[1], 16)
|
|
elif p[0] == "PAID" and len(p) > 1: d["paid"] = int(p[1])
|
|
elif p[0] == "VER": d["ver"] = p[1] if len(p) > 1 and p[1] != "DIGEST" else ""; d["digest"] = p[-1] if "DIGEST" in p else ""
|
|
return d
|
|
with ThreadPoolExecutor(8) as ex: res = list(ex.map(read, rows))
|
|
# a common height for the state root: the lowest exec tip minus a little
|
|
execs = [d.get("exec", 0) for d in res if d.get("exec")]; h = max(0, min(execs) - 2) if execs else 0
|
|
def root(d):
|
|
rc, out, err = d["box"].run(f"curl -s -m 6 -X POST -H 'Content-Type: application/json' --data '{{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getBlockByNumber\",\"params\":[\"{hex(h)}\",false]}}' http://127.0.0.1:{d['evm']}/ | grep -oE '\"stateRoot\":\"0x[0-9a-f]+\"' | cut -d'\"' -f4", 30); return out.strip()[:18]
|
|
with ThreadPoolExecutor(8) as ex:
|
|
for d, r in zip(res, ex.map(root, res)): d["root"] = r
|
|
for d in res: print(f" {d['label']:<10} {'seed ' if d['seed'] else ' '} blocks={d.get('blocks','?'):<7} daa={d.get('daa','?'):<7} synced={d.get('synced','?'):<5} exec={d.get('exec','?'):<7} root@{h}={d.get('root','?'):<18} paid={d.get('paid','?'):<4} rej={d.get('rej','?')} reorg={d.get('reorg','?')} {d.get('ver','')} {d.get('digest','')}")
|
|
rej = sum(d.get("rej", 0) for d in res); reorg = max((d.get("reorg", 0) for d in res), default=0); roots = {d.get("root") for d in res if d.get("root")}; vers = {d.get("ver") for d in res if d.get("ver")}; paid = max((d.get("paid", 0) for d in res), default=0)
|
|
ok = rej == 0 and reorg <= 3 and len(roots) == 1 and paid >= 1 and len(vers) == 1
|
|
print(f"{'PASS' if ok else 'FAIL'} {now()} since {since}: boxes {len(res)}, rejected {rej}, max reorg {reorg}, state roots at {h}: {len(roots)} distinct, paid segments {paid}, versions {sorted(vers)}")
|