igneum/infra/devnet/crossing-watch.mjs
2026-10-06 00:35:45 +00:00

100 lines
6.9 KiB
JavaScript

#!/usr/bin/env node
// Counter ASIC 2.0 (6 October 2026): the unattended watch of the devnet's crossing into program class v3 at
// N4 = 154,800 (epoch 43), armed before the night's coordinator stops (consequences C45). Reads the observer's
// JSON-RPC (ws://127.0.0.1:28640, read-only: getBlockDagInfo and getBlockTemplate) every POLL seconds and the fleet's
// STATUS lines through `node tools/jobs.mjs status` every 5 minutes, and writes RESULT lines the morning reads first:
// RESULT epoch <e> class <v2|v3> generator <n> era <i> seed <hex16> at daa <d> <time> every epoch change
// RESULT crossing ... the first epoch at or above N4
// RESULT chain <before|after> daa <d> blocks <n> ... rate <blocks per minute> the chain's advance each side
// RESULT fleet <time> <machine> <MH/s> ... the STATUS rates (tools/jobs.mjs status)
// RESULT verdict PASS|FAIL <reason> when the after-window closes
// FAIL when: no block for 10 minutes after the crossing, the class after the crossing is not v3, the generator is not 3,
// the template stops answering, or the fleet's total rate after the crossing is under a quarter of the rate before.
// Never touches a node, a miner or a job. Stops on its own after AFTER_MIN minutes past the crossing (or at the deadline).
// node infra/devnet/crossing-watch.mjs [--n4 154800] [--after-min 90] [--deadline-utc 2026-10-06T08:00:00Z]
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
import { devAddress } from '../../tools/harness/lib/address.mjs';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const arg = (k, d) => { const i = process.argv.indexOf(k); return i > 0 ? process.argv[i + 1] : d; };
const N4 = Number(arg('--n4', 154800));
const AFTER_MIN = Number(arg('--after-min', 90));
const DEADLINE = Date.parse(arg('--deadline-utc', '2026-10-06T08:00:00Z'));
const POLL = 15;
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const pay = devAddress('crossing-watch');
const now = () => new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
const out = (s) => { process.stdout.write(`${s}\n`); };
const result = (s) => out(`RESULT ${s}`);
let rpc;
async function connect() { rpc = await connectRpc('ws://127.0.0.1:28640'); }
async function read() {
const d = await rpc.call('getBlockDagInfo', {});
const t = await rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] });
const pe = t.powEpoch || t.pow_epoch || {};
return { daa: Number(d.virtualDaaScore), blocks: Number(d.blockCount), sink: String(d.sink).slice(0, 16),
epoch: Number(pe.epochIndex), cls: Number(pe.programClass), next: Number(pe.nextProgramClass), era: Number(pe.eraIndex),
eraSeed: String(pe.eraSeed || '').slice(0, 16), act: Number(pe.programClassV3ActivationDaa), boundary: Number(pe.boundaryDaaScore) };
}
function fleet() {
try {
const s = execFileSync('node', [path.join(ROOT, 'tools/jobs.mjs'), 'status'], { encoding: 'utf8', timeout: 60000 });
return s.split('\n').filter(l => /^\S+-[0-9a-f]{8}\s/.test(l)).map(l => l.replace(/\s+/g, ' ').slice(0, 160));
} catch (e) { return [`fleet read failed: ${e.message.split('\n')[0]}`]; }
}
const clsName = (c) => (c === 3 ? 'v3' : c === 2 ? 'v2' : `class${c}`);
out(`crossing-watch started ${now()} N4 ${N4} after-window ${AFTER_MIN} min deadline ${new Date(DEADLINE).toISOString()} observer ws://127.0.0.1:28640`);
await connect();
let last = null, crossing = null, lastBlockAt = Date.now(), lastFleet = 0, failures = 0, verdict = null;
const before = [], after = [];
let misses = 0;
while (Date.now() < DEADLINE) {
let r;
try { r = await read(); misses = 0; } catch (e) {
misses++; out(`${now()} template/dag read failed (${misses}): ${e.message}`);
if (misses % 4 === 0) { try { await connect(); } catch {} }
if (misses >= 40) { result(`verdict FAIL the observer stopped answering for ${misses * POLL} s at ${now()}`); verdict = 'FAIL'; break; }
await new Promise(s => setTimeout(s, POLL * 1000)); continue;
}
if (!last || r.blocks !== last.blocks) lastBlockAt = Date.now();
if (!last || r.epoch !== last.epoch) {
result(`epoch ${r.epoch} class ${clsName(r.cls)} generator ${r.cls} next ${clsName(r.next)} era ${r.era} seed ${r.eraSeed} activation ${r.act} boundary ${r.boundary} at daa ${r.daa} blocks ${r.blocks} ${now()}`);
if (!crossing && r.daa >= N4) {
crossing = { at: Date.now(), daa: r.daa, blocks: r.blocks, epoch: r.epoch, cls: r.cls };
result(`crossing epoch ${r.epoch} class ${clsName(r.cls)} generator ${r.cls} at daa ${r.daa} blocks ${r.blocks} ${now()} (N4 ${N4})`);
if (r.cls !== 3) { result(`verdict FAIL the first epoch at or above N4 is class ${clsName(r.cls)}, not v3, at ${now()}`); verdict = 'FAIL'; }
result(`fleet ${now()} at the crossing: ${fleet().join(' | ')}`);
}
}
const side = crossing ? after : before;
side.push({ t: Date.now(), daa: r.daa, blocks: r.blocks });
if (Date.now() - lastFleet > 5 * 60 * 1000) {
lastFleet = Date.now();
const w = side.length > 1 ? side[side.length - 1] : null, w0 = side.length > 20 ? side[side.length - 21] : side[0];
const rate = w && w.t > w0.t ? ((w.blocks - w0.blocks) / ((w.t - w0.t) / 60000)).toFixed(1) : 'n/a';
result(`chain ${crossing ? 'after' : 'before'} daa ${r.daa} blocks ${r.blocks} sink ${r.sink} epoch ${r.epoch} class ${clsName(r.cls)} rate ${rate} blocks/min ${now()}`);
result(`fleet ${now()}: ${fleet().join(' | ')}`);
}
if (crossing && !verdict && Date.now() - lastBlockAt > 10 * 60 * 1000) {
result(`verdict FAIL no block on the observer for 10 minutes after the crossing (last at ${new Date(lastBlockAt).toISOString()}, daa ${r.daa}) ${now()}`); verdict = 'FAIL';
}
if (crossing && !verdict && Date.now() - crossing.at > AFTER_MIN * 60 * 1000) {
const b0 = before.length > 40 ? before[before.length - 41] : before[0], b1 = before[before.length - 1];
const rb = b1 && b0 && b1.t > b0.t ? (b1.blocks - b0.blocks) / ((b1.t - b0.t) / 60000) : null;
const a0 = after[0], a1 = after[after.length - 1];
const ra = a1 && a0 && a1.t > a0.t ? (a1.blocks - a0.blocks) / ((a1.t - a0.t) / 60000) : null;
const ok = r.cls === 3 && ra != null && (rb == null || ra >= rb / 4);
result(`verdict ${ok ? 'PASS' : 'FAIL'} class ${clsName(r.cls)} after the crossing; blocks/min before ${rb?.toFixed(1) ?? 'n/a'} after ${ra?.toFixed(1) ?? 'n/a'}; daa ${r.daa} blocks ${r.blocks} ${now()}`);
verdict = ok ? 'PASS' : 'FAIL';
}
if (verdict && crossing && Date.now() - crossing.at > AFTER_MIN * 60 * 1000) break;
last = r;
await new Promise(s => setTimeout(s, POLL * 1000));
}
if (!verdict) result(`verdict NONE the deadline passed at ${now()} ${crossing ? 'after the crossing' : 'before any crossing'} (last daa ${last?.daa})`);
out(`crossing-watch stopped ${now()}`);
process.exit(0);