77 lines
5.1 KiB
Python
77 lines
5.1 KiB
Python
#!/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)))
|