igneum/tools/fleet/bps-collect.py

55 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""The block-rate run's collector (docs/analysis/block-rate-devnet2.md). Every 60 s, from the Devnet 2 seed and every
Devnet 2 miner box (the library's chain-side readers): height, daa, blue score, tips, peers, exec tip; the seed's
counts of accepted blocks and red blocks, reorg depths, finality lock lines (delay from determined to LOCKED, voters
and weight), p2p bytes (the node's /proc/<pid>/net or ss), CPU and RSS of the node. One JSON line per minute to
~/Desktop/fleet/bps/<run>.jsonl; `--report <run>` prints the run's table and the payout intervals per tier.
"""
import sys, os, json, time, datetime, re
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))); from lib import Box, Registry
ROOT = os.path.expanduser("~/Desktop/fleet/bps"); os.makedirs(ROOT, exist_ok=True)
def now(): return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
SEED_CMD = r"""L=${SEED_LOG:-/root/fleet/dn2-node.log}; P=$(pgrep -f "${SEED_PAT:-^/root/fleet/in/igneumd}" | head -1)
echo accepted=$(grep -c 'PoW accepted' $L) red=$(grep -ciE 'red block|colored red|is red' $L) reorg_max=$(grep -oE 'selected-chain reorg: [0-9]+ chain blocks' $L | grep -oE '[0-9]+ chain' | awk '{if ($1>m) m=$1} END {print m+0}') locks=$(grep -c 'LOCKED' $L)
grep -E 'Finality: (checkpoint [0-9]+ determined|checkpoint [0-9]+ LOCKED|certificate at index [0-9]+ received)' $L | tail -40 | sed -E 's/^([0-9-]+ [0-9:.]+)[^]]*\] //' | cut -c1-170
echo cpu_rss=$(ps -o %cpu=,rss= -p $P 2>/dev/null | tr -s ' ') rx_tx=$(cat /proc/$P/net/dev 2>/dev/null | awk 'NR>2 && $1!="lo:" {rx+=$2; tx+=$10} END {print rx, tx}')
${SEED_MINER:-/opt/igneum/pkg/bin/igneum-miner} watch 1 grpc://127.0.0.1:${SEED_RPC:-26610} 2>/dev/null | grep -o 'blocks=[0-9]*.*synced=[a-z]*' | tail -1
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:${SEED_EVM:-26790}/ | python3 -c 'import sys,json; r=sys.stdin.read(); d=json.loads(r).get("result",{}) if r.strip() else {}; print("exec", int(d.get("executedTip","0x0"),16))' 2>/dev/null"""
def sample(run):
reg = Registry.load()
sb = next((b for i, b in reg.items() if b.get("dn2_seed") and b.get("state") != "destroyed"), None)
seed = Box(sb["ssh_host"], sb["ssh_port"], sb["label"], None, sb.get("wallet"), sb.get("provider"), sb.get("ssh_user", "root")) if sb else None; seed_env = (sb or {}).get("seed_env")
row = {"t": now(), "run": run}
if seed:
env = " ".join(f"{k}={v}" for k, v in (seed_env or {}).items()); rc, out, err = seed.run((env + " " if env else "") + SEED_CMD, 60); row["seed_raw"] = out[-4000:]
for l in out.split("\n"):
if l.startswith("accepted="): row.update(dict(kv.split("=") for kv in l.split()))
elif l.startswith("blocks="):
row.update({"w_" + k: v for k, v in (kv.split("=") for kv in l.split() if "=" in kv)})
try: row["red_share"] = round(1 - int(row["w_blue"]) / max(int(row["w_daa"]), 1), 4) # the node logs no red line; red = 1 - blue score / DAA score
except Exception: pass
elif l.startswith("exec "): row["exec_tip"] = int(l.split()[1])
elif l.startswith("cpu_rss="): row["cpu_rss"] = l
fin = [l for l in out.split("\n") if "Finality:" in l]; row["finality_tail"] = fin[-12:]
miners = [b for b in reg.values() if b.get("state") != "destroyed" and b.get("ssh_host") and (b.get("devnet2") or b.get("stage") == "bps")]
row["miner_boxes"] = len(miners)
with open(os.path.join(ROOT, f"{run}.jsonl"), "a") as f: f.write(json.dumps(row) + "\n")
print(row["t"], run, {k: row.get(k) for k in ("accepted", "red", "reorg_max", "locks", "w_blocks", "w_daa", "w_blue", "w_tips", "w_peers", "exec_tip", "miner_boxes")}, flush=True)
def report(run):
rows = [json.loads(l) for l in open(os.path.join(ROOT, f"{run}.jsonl"))]
if len(rows) < 2: print("too few rows"); return
a, z = rows[0], rows[-1]
secs = (datetime.datetime.strptime(z["t"], "%Y-%m-%dT%H:%M:%SZ") - datetime.datetime.strptime(a["t"], "%Y-%m-%dT%H:%M:%SZ")).total_seconds()
blocks = int(z.get("w_blocks", 0)) - int(a.get("w_blocks", 0)); blue = int(z.get("w_blue", 0)) - int(a.get("w_blue", 0))
print(f"run {run}: {secs/60:.1f} min, blocks {blocks} ({blocks/max(secs,1):.2f}/s), blue score +{blue} ({blue/max(secs,1):.2f}/s), red share {100*(1-blue/max(blocks,1)):.1f}%, max reorg {max(int(r.get('reorg_max',0)) for r in rows)}, exec lag at end {int(z.get('w_blocks',0)) - int(z.get('exec_tip',0))} blocks, locks {int(z.get('locks',0)) - int(a.get('locks',0))}")
# payout interval per tier at the measured block rate: blocks/s x (miner hash / network hash)
bps = blocks / max(secs, 1)
for net_name, net in (("tonight (2.0 GH/s)", 2.0e9), ("1 TH/s", 1e12), ("10 TH/s", 1e13), ("100 TH/s", 1e14)):
for card, mhs in (("4070 (28 MH/s)", 28e6), ("5090 (128 MH/s)", 128e6), ("8x 4090 rig (459 MH/s)", 459e6)):
per_s = bps * mhs / net; iv = 1 / per_s if per_s else float("inf")
print(f" {net_name:<20} {card:<24} one block every {iv/60:,.1f} min ({iv/3600:,.2f} h)")
if __name__ == "__main__":
if sys.argv[1] == "--report": report(sys.argv[2])
else:
run = sys.argv[1]
while True: sample(run); time.sleep(60)