#!/usr/bin/env python3 """F22 measurement: vote issue time against the certificate cut-off on the 12-node cloud devnet (4 Oct 2026). Sources: nodes/igneum-XX/miner.log (VOTE lines, unix ms) and igneumd.log.gz (determined / certificate built / LOCKED).""" import gzip, re, sys, statistics as st from datetime import datetime, timezone base = sys.argv[1] t_from = datetime.fromisoformat(sys.argv[2]).replace(tzinfo=timezone.utc).timestamp() t_to = datetime.fromisoformat(sys.argv[3]).replace(tzinfo=timezone.utc).timestamp() nodes = [f"igneum-{i:02d}" for i in range(1, 13)] det, built, lock, vote = {}, {}, {}, {} re_node = re.compile(r"(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d+)\+00:00 \[INFO \] Finality: (.*)") for n in nodes: det[n], built[n], lock[n], vote[n] = {}, {}, {}, {} with gzip.open(f"{base}/nodes/{n}/igneumd.log.gz", "rt", errors="replace") as f: for line in f: m = re_node.search(line) if not m: continue t = datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S.%f").replace(tzinfo=timezone.utc).timestamp() msg = m.group(2) mm = re.match(r"checkpoint (\d+) determined", msg) if mm: det[n].setdefault(int(mm.group(1)), t); continue mm = re.match(r"certificate built for checkpoint (\d+) .* by (\d+) of (\d+) voters", msg) if mm: built[n].setdefault(int(mm.group(1)), (t, int(mm.group(2)), int(mm.group(3)))); continue mm = re.match(r"checkpoint (\d+) LOCKED: .* signed (\d+) = ([\d.]+)% of active, ([\d.]+)% of total", msg) if mm: lock[n].setdefault(int(mm.group(1)), (t, float(mm.group(4)))); continue with open(f"{base}/nodes/{n}/miner.log", errors="replace") as f: for line in f: mm = re.search(r"(\d+\.\d+) VOTE index=(\d+)", line) if mm: vote[n].setdefault(int(mm.group(2)), float(mm.group(1))) idxs = sorted(set.union(*[set(det[n]) for n in nodes])) rows = [] for i in idxs: d = [det[n][i] for n in nodes if i in det[n]] if not d: continue t0 = min(d) if t0 < t_from or t0 > t_to: continue v = sorted(vote[n][i] for n in nodes if i in vote[n]) b = sorted((built[n][i][0], built[n][i][1], n) for n in nodes if i in built[n]) if len(v) < 12 or not b: continue tc, nc, who = b[0] before = sum(1 for x in v if x <= tc) # the certificate every node locked: the signer fraction from the LOCKED lines (fraction of total), median over nodes fr = [lock[n][i][1] for n in nodes if i in lock[n]] rows.append(dict(i=i, t0=t0, det_spread=max(d) - t0, v_first=v[0] - t0, v_med=v[len(v)//2] - t0, v_last=v[-1] - t0, tc=tc - t0, nc=nc, before=before, builders=len(b), frac=st.median(fr) if fr else None, locked_nodes=len(fr))) def q(xs, p): xs = sorted(xs); return xs[min(len(xs)-1, int(p * len(xs)))] print(f"indices measured: {len(rows)} (first {rows[0]['i']}, last {rows[-1]['i']}), window {sys.argv[2]} to {sys.argv[3]} UTC") print() print("| quantity (seconds after the earliest determination of the index) | median | p90 | p99 | max |") print("|---|---|---|---|---|") for k, lab in [("det_spread", "determination spread across the 12 nodes"), ("v_first", "first vote issued"), ("v_med", "median vote issued"), ("v_last", "last of the 12 votes issued"), ("tc", "first certificate built (the cut-off)")]: xs = [r[k] for r in rows] print(f"| {lab} | {st.median(xs):.2f} | {q(xs,0.9):.2f} | {q(xs,0.99):.2f} | {max(xs):.2f} |") print() nc = [r["nc"] for r in rows]; bf = [r["before"] for r in rows] from collections import Counter print("signers in the first-built certificate:", dict(sorted(Counter(nc).items())), "mean %.2f of 12" % st.mean(nc)) print("votes issued at or before the first build:", dict(sorted(Counter(bf).items())), "mean %.2f of 12" % st.mean(bf)) print("builders per index (nodes that built their own certificate):", dict(sorted(Counter(r['builders'] for r in rows).items()))) fr = [r["frac"] for r in rows if r["frac"] is not None] print("locked fraction of total (median over nodes): min %.1f%% median %.1f%% max %.1f%%; indices under 70%%: %d of %d" % (min(fr), st.median(fr), max(fr), sum(1 for x in fr if x < 70), len(fr))) # how long a hold after quorum would have been needed to carry k of 12 votes print() print("| hold after the first build (s) | indices where all 12 votes were issued by then | 11 or more |") print("|---|---|---|") for h in (0, 0.25, 0.5, 1.0, 1.5, 2.0, 3.0, 5.0): all12 = sum(1 for r in rows if r["v_last"] <= r["tc"] + h) ge11 = sum(1 for r in rows if sorted([x for x in [vote[n][r['i']] - r['t0'] for n in nodes if r['i'] in vote[n]]])[-2] <= r["tc"] + h) print(f"| {h} | {all12} of {len(rows)} ({100*all12/len(rows):.0f}%) | {ge11} ({100*ge11/len(rows):.0f}%) |") # vote issue lag relative to the voter's own node's determination (the miner's 1-s poll) own = [] for r in rows: for n in nodes: if r['i'] in vote[n] and r['i'] in det[n]: own.append(vote[n][r['i']] - det[n][r['i']]) print() print("vote issue lag after the voter's own node determined the checkpoint (the miner's 1-s poll): median %.2f p90 %.2f p99 %.2f max %.2f s, n=%d" % (st.median(own), q(own,0.9), q(own,0.99), max(own), len(own)))