305 lines
23 KiB
JavaScript
305 lines
23 KiB
JavaScript
#!/usr/bin/env node
|
|
// Counter ASIC 3.0 gate G4 (docs/plans/counter-asic-3-node.md; the 2.0 shape: docs/plans/counter-asic-2-rollout.md
|
|
// section 7): the fast-time 3-node network mining across a program class v4 activation. A private network on
|
|
// 127.0.0.1 ports 29660 and up, data under /tmp/igneum-fast-time-v4, network id igneum-devnet-966, every node on
|
|
// infra/fast-time/override-60x.json merged with a CPU genesis difficulty (genesis_bits 0x1f010000, 2^16 hashes per
|
|
// block, as sim/difficulty/testnet_v2.py), `program_class_v3_activation_daa` at --v3-activation (default 60: epoch 1,
|
|
// so the run crosses v2 -> v3 -> v4 and the two switches are both exercised) and `program_class_v4_activation_daa`
|
|
// at --activation (default 150: inside epoch 2 at 60 DAA per epoch, so the switch rounds UP to epoch 3 at DAA 180,
|
|
// the boundary rule under test). One real CPU miner per node (igneum-miner --engine igneum-pow, 1 thread) follows
|
|
// its node's templates, so every block of the run is a real lottery-hash solution and every node verifies every
|
|
// block of the other two under the class of its epoch.
|
|
//
|
|
// Reports, from the nodes' RPC and the logs: blocks on each side of the v4 boundary, the program class and id of
|
|
// every epoch (before and after), rejected blocks (the miners' submit answers and the nodes' "PoW rejected" lines),
|
|
// forks (every node's sink, selected tip and block count at the end), and every node's two switch lines. Exit 0 when
|
|
// every check passes. Never touches the live devnet (26610/26611, 26640/26641, 28640), the class-v3 harness (29600)
|
|
// or the simnet.mjs ports.
|
|
//
|
|
// The known-failed case (the gate rule: a gate is trusted only after it fires on one finished and one failed case):
|
|
// `--activation never` leaves the v4 switch at never; the harness must then report FAIL with no v4 epoch seen.
|
|
//
|
|
// node infra/fast-time/class-v4.mjs [--secs 420] [--activation 150|never] [--v3-activation 60|never] [--epochs-after 2]
|
|
// [--threads 1] [--metal <igneum-bench>] [--genesis-bits 0x1f010000]
|
|
// IGNEUMD and IGNEUM_MINER name the binaries (default: the ca3 fork worktree's target-ca3v4/release).
|
|
// --metal: node 0's miner is a real Metal GPU worker (igneum-miner --worker <igneum-bench> --prepare-packs <dir>
|
|
// --exit-on-seed-change, the app's own shape), so the Metal worker's class v4 path (the prepare line with the pack
|
|
// directory and the class and era tokens, the pack-built program and day) mines across the boundary (gate G4b);
|
|
// the report then adds the miner's PREPARE lines, the worker's prepared lines, any need / mismatch / refusal line,
|
|
// the accepted blocks on each side and the swap time. --genesis-bits raises the CPU difficulty for a GPU miner
|
|
// (0x1e010000 = 2^24 hashes per block: under a second on an M5 Max; the CPU miners on nodes 1 and 2 then verify
|
|
// and rarely find).
|
|
|
|
import { spawn } from 'node:child_process';
|
|
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
|
|
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
|
|
import { devAddress } from '../../tools/harness/lib/address.mjs';
|
|
|
|
const ROOT = new URL('../../', import.meta.url).pathname;
|
|
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
|
|
const BIN = process.env.IGNEUM_CA3_BIN || `${ROOT}vendor/igneum-node-ca3v4/target-ca3v4/release`;
|
|
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
|
|
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
|
|
const TMP = '/tmp/igneum-fast-time-v4';
|
|
const BASE = 29660, SUFFIX = 966;
|
|
const NEVER = '18446744073709551615';
|
|
const args = process.argv.slice(2);
|
|
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? +args[i + 1] : dflt; };
|
|
const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
|
|
// an activation flag: a DAA score, or "never" (the known-failed case for the v4 switch; a v2-only chain for the v3 one)
|
|
const actflag = (name, dflt) => { const v = sflag(name); if (v == null) return dflt; return v === 'never' ? null : +v; };
|
|
const GENESIS_BITS = flag('genesis-bits', 0x1f010000);
|
|
const METAL = sflag('metal');
|
|
const SECS = flag('secs', 420);
|
|
const ACTIVATION = actflag('activation', 150); // class v4 (N5); null = never
|
|
const V3_ACTIVATION = actflag('v3-activation', 60); // class v3 (N4); null = never
|
|
const EPOCHS_AFTER = flag('epochs-after', 2);
|
|
const THREADS = flag('threads', 1);
|
|
const started = [];
|
|
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
|
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
for (const b of [IGNEUMD, CPU_MINER, ...(METAL ? [METAL] : [])]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
|
|
|
|
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
|
// The file is merged as TEXT, never through JSON.parse: a `never` height is 18446744073709551615, which JavaScript
|
|
// rounds to 1.8446744073709552e+19 and the node refuses ("expected u64"; the first gate run, 5 October 2026 21:30Z).
|
|
// The merged fields are appended after the file's last field; a field already in the file is removed first.
|
|
const baseText = readFileSync(FILE, 'utf8');
|
|
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
|
|
const EPOCH = field('pow_epoch_blocks');
|
|
const DAY_MS = field('pow_day_ms');
|
|
const FIRST_V4_EPOCH = ACTIVATION == null ? null : Math.ceil(ACTIVATION / EPOCH);
|
|
const FIRST_V3_EPOCH = V3_ACTIVATION == null ? null : Math.ceil(V3_ACTIVATION / EPOCH);
|
|
const BOUNDARY = FIRST_V4_EPOCH == null ? Infinity : FIRST_V4_EPOCH * EPOCH;
|
|
const override = `${TMP}/override.json`;
|
|
export function mergeOverrideText(text, fields) {
|
|
let out = text;
|
|
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
|
|
// a string of digits is written raw (a never height is the u64 max, beyond a JavaScript number); other strings quoted
|
|
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
|
|
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
|
|
}
|
|
// the two heights as digit text (the file's own two fields, both at never, are removed first by the merge)
|
|
const asText = (v) => v == null ? NEVER : String(v);
|
|
writeFileSync(override, mergeOverrideText(baseText, { genesis_bits: GENESIS_BITS, skip_proof_of_work: false, program_class_v3_activation_daa: asText(V3_ACTIVATION), program_class_v4_activation_daa: asText(ACTIVATION) }));
|
|
log(`v3 activation ${V3_ACTIVATION ?? 'never'} (first v3 epoch ${FIRST_V3_EPOCH ?? 'none'}), v4 activation ${ACTIVATION ?? 'never'} at ${EPOCH} DAA per epoch: the first v4 epoch is ${FIRST_V4_EPOCH ?? 'none'} (DAA ${Number.isFinite(BOUNDARY) ? BOUNDARY : 'never'}); run ${SECS} s, CPU genesis bits 0x${GENESIS_BITS.toString(16)}`);
|
|
|
|
class Node {
|
|
constructor(i, connect = []) {
|
|
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2;
|
|
this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`;
|
|
}
|
|
get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; }
|
|
async start() {
|
|
mkdirSync(this.dir, { recursive: true });
|
|
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex',
|
|
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`,
|
|
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
|
|
if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0');
|
|
const out = openSync(this.logFile, 'a');
|
|
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out] });
|
|
started.push(this.proc);
|
|
await sleep(1200);
|
|
this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`);
|
|
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}`);
|
|
return this;
|
|
}
|
|
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
|
|
}
|
|
function miner(bin, argv, name, env = {}) {
|
|
const out = openSync(`${TMP}/${name}.log`, 'a');
|
|
const p = spawn(bin, argv, { stdio: ['ignore', out, out], env: { ...process.env, ...env } });
|
|
started.push(p);
|
|
return p;
|
|
}
|
|
async function stopAll() {
|
|
for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch { } }
|
|
await sleep(1500);
|
|
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
|
|
}
|
|
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
|
|
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); }); // a thrown start leaves no node behind
|
|
const minerName = (i) => (METAL && i === 0) ? 'metal0' : `cpu${i}`;
|
|
const minerLog = (i) => { try { return readFileSync(`${TMP}/${minerName(i)}.log`, 'utf8').split('\n'); } catch { return []; } };
|
|
const V4_LINE = /Program class v4 from the override file/, V3_LINE = /Program class v3 from the override file/;
|
|
const tail = (l, what) => l.replace(new RegExp(`^.*?(Program class ${what})`), '$1');
|
|
|
|
const t0 = Date.now();
|
|
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
|
|
const n0 = await new Node(0).start();
|
|
const n1 = await new Node(1, [`127.0.0.1:${n0.p2pPort}`]).start();
|
|
const n2 = await new Node(2, [`127.0.0.1:${n0.p2pPort}`]).start(); // --connect takes one address
|
|
const nodes = [n0, n1, n2];
|
|
for (const n of nodes) {
|
|
const v3 = n.grepLog(V3_LINE)[0], v4 = n.grepLog(V4_LINE)[0];
|
|
log(`n${n.i} switch lines: ${v3 ? tail(v3, 'v3') : '(no v3 line)'} | ${v4 ? tail(v4, 'v4') : '(no v4 line)'}`);
|
|
}
|
|
log(`n0 PoW schedule: ${n0.grepLog(/PoW schedule from the override file/).map(l => l.replace(/^.*?(PoW schedule)/, '$1')).join(' | ') || '(no line)'}`);
|
|
log(`n0 digest: ${n0.grepLog(/Consensus params digest/).map(l => l.replace(/^.*?digest: /, '').slice(0, 16)).join(' ')}`);
|
|
|
|
// one real CPU miner per node, 1 thread, 2^16 expected hashes per block at genesis; with --metal, node 0's miner drives
|
|
// the Metal worker the way the app does (the pack for every prepared pair under packs/prepare, exit 42/44 on a refusal)
|
|
const PACKS = `${TMP}/packs/prepare`;
|
|
mkdirSync(PACKS, { recursive: true });
|
|
nodes.forEach((n, i) => {
|
|
if (METAL && i === 0) {
|
|
miner(CPU_MINER, ['mine', n.grpc, '1', String(SECS), 'metal0', '--engine', 'igneum-pow', '--worker', METAL, '--prepare-packs', PACKS, '--exit-on-seed-change', '--payout-label', 'metal0', '--status-secs', '30', '--no-vote'], 'metal0', { IGNEUM_POW_DAY_MS: String(DAY_MS) });
|
|
log(`n0 miner: Metal worker ${METAL}, packs under ${PACKS}`);
|
|
} else {
|
|
miner(CPU_MINER, ['mine', n.grpc, String(THREADS), String(SECS), `cpu${i}`, '--engine', 'igneum-pow', '--payout-label', `cpu${i}`, '--status-secs', '30', '--no-vote'], `cpu${i}`, { IGNEUM_POW_DAY_MS: String(DAY_MS) });
|
|
}
|
|
});
|
|
const pay = devAddress('fast-time-v4');
|
|
|
|
const epochs = new Map(); // epoch index -> { class, firstSeenDaa, at }
|
|
let firstV4 = null, firstV3 = null, lastEpoch = -1, lastReport = 0, lastDaa = 0, switchEnd = null;
|
|
const samples = [];
|
|
while (Date.now() - t0 < SECS * 1000) {
|
|
await sleep(1000);
|
|
let daa = null, epoch = null, cls = null, nextCls = null, era = null, eraSeed = null, act3 = null, act4 = null;
|
|
try {
|
|
const t = await n0.rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] });
|
|
const pe = t.powEpoch || t.pow_epoch || {};
|
|
daa = pe.virtualDaaScore ?? t.block?.header?.daaScore; epoch = pe.epochIndex; cls = pe.programClass; nextCls = pe.nextProgramClass;
|
|
era = pe.eraIndex; eraSeed = pe.eraSeed; act3 = pe.programClassV3ActivationDaa; act4 = pe.programClassV4ActivationDaa;
|
|
} catch (e) { log(`template: ${e.message}`); }
|
|
if (epoch != null && epoch !== lastEpoch) {
|
|
epochs.set(epoch, { class: cls, firstSeenDaa: daa, at: +since() });
|
|
log(`epoch ${lastEpoch} -> ${epoch} at daa ${daa}, ${since()} s: template class ${cls} (generator), next epoch class ${nextCls}, era ${era} seed ${String(eraSeed).slice(0, 16)}, activation v3 ${act3} v4 ${act4}`);
|
|
if (firstV3 == null && cls === 3) { firstV3 = { epoch, daa, at: +since() }; log(`CLASS SWITCH: the template is class v3 from epoch ${epoch} (daa ${daa}) at ${since()} s wall`); }
|
|
if (firstV4 == null && cls === 4) { firstV4 = { epoch, daa, at: +since() }; log(`CLASS SWITCH: the template is class v4 from epoch ${epoch} (daa ${daa}) at ${since()} s wall`); }
|
|
lastEpoch = epoch;
|
|
}
|
|
lastDaa = daa ?? lastDaa;
|
|
if (Date.now() - lastReport > 15000) {
|
|
lastReport = Date.now();
|
|
const counts = await Promise.all(nodes.map(async n => { try { const d = await n.rpc.call('getBlockDagInfo'); return `${d.blockCount}/${String(d.sink).slice(0, 8)}`; } catch { return '?'; } }));
|
|
log(`t=${since()} s daa ${daa} epoch ${epoch} class ${cls} blocks/sink per node ${counts.join(' ')}`);
|
|
samples.push({ t: +since(), daa, epoch, class: cls, nodes: counts });
|
|
}
|
|
if (firstV4 != null && daa != null && daa >= BOUNDARY + EPOCHS_AFTER * EPOCH) { switchEnd = +since(); break; }
|
|
}
|
|
await sleep(3000); // let the last blocks relay before the end-of-run reads
|
|
|
|
// ---- the end-of-run reads -----------------------------------------------------------------------------------------
|
|
const dag = await Promise.all(nodes.map(async n => { try { return await n.rpc.call('getBlockDagInfo'); } catch (e) { return { error: e.message }; } }));
|
|
const genesis = dag[0].pruningPointHash;
|
|
// every block the first node holds, by DAA score, from genesis
|
|
async function allBlocks(n) {
|
|
const out = []; let low = genesis; const seen = new Set();
|
|
for (let round = 0; round < 500; round++) {
|
|
const r = await n.rpc.call('getBlocks', { lowHash: low, includeBlocks: true, includeTransactions: false });
|
|
const blocks = r.blocks || [];
|
|
let added = 0;
|
|
for (const b of blocks) { const h = b.verboseData?.hash || b.header?.hash; if (seen.has(h)) continue; seen.add(h); out.push({ hash: h, daa: +b.header.daaScore, chain: !!b.verboseData?.isChainBlock, blue: +(b.verboseData?.blueScore ?? 0) }); added++; }
|
|
if (!blocks.length || added === 0) break;
|
|
low = (r.blockHashes || []).at(-1) || blocks.at(-1).verboseData?.hash; if (!low) break;
|
|
}
|
|
return out;
|
|
}
|
|
let blocks = [];
|
|
try { blocks = await allBlocks(n0); } catch (e) { log(`getBlocks: ${e.message}`); }
|
|
const before = blocks.filter(b => b.daa < BOUNDARY), after = blocks.filter(b => b.daa >= BOUNDARY);
|
|
const chainBefore = before.filter(b => b.chain).length, chainAfter = after.filter(b => b.chain).length;
|
|
|
|
// program ids and classes per epoch from the miners' "program and 256 MiB cache ready" lines
|
|
const programs = new Map(); // epoch seed -> { class, id, miners: Set }
|
|
for (const i of [0, 1, 2]) for (const l of minerLog(i)) {
|
|
const m = /epoch seed ([0-9a-f]{64}) day (\d+) \(daa (\d+)\): program and 256 MiB cache ready in ([\d.]+) ms; class (v\d) program id ([0-9a-f]{16})/.exec(l);
|
|
if (!m) continue;
|
|
const k = m[1]; const e = programs.get(k) || { seed: k.slice(0, 16), epoch: Math.floor(+m[3] / EPOCH), class: m[5], id: m[6], miners: new Set(), ready_ms: [] };
|
|
if (e.id !== m[6] || e.class !== m[5]) e.disagree = true;
|
|
e.miners.add(i); e.ready_ms.push(+m[4]); programs.set(k, e);
|
|
}
|
|
// ready_ms: the program generation plus the day's cache and dataset build on one CPU core (the first epoch of a day
|
|
// pays the cache; a v4 epoch after a v3 one reuses the v3 day cache, so its number is the program alone)
|
|
const programRows = [...programs.values()].sort((a, b) => a.epoch - b.epoch).map(p => ({ epoch: p.epoch, class: p.class, program_id: p.id, seed: p.seed, miners: p.miners.size, disagree: !!p.disagree, ready_ms: p.ready_ms }));
|
|
const idsOf = (c) => programRows.filter(p => p.class === c).map(p => p.program_id);
|
|
const v2Ids = idsOf('v2'), v3Ids = idsOf('v3'), v4Ids = idsOf('v4');
|
|
const olderIds = [...v2Ids, ...v3Ids];
|
|
|
|
// rejections: the miners' submit answers and the nodes' PoW lines
|
|
const accepted = [0, 1, 2].map(i => minerLog(i).filter(l => /ACCEPTED block/.test(l)).length);
|
|
const rejectedMiner = [0, 1, 2].map(i => minerLog(i).filter(l => /rejected nonce=|submit error/.test(l)));
|
|
const rejectedNode = nodes.map(n => n.grepLog(/PoW rejected|Rejected block|rejected block/i));
|
|
const switchLines = nodes.map(n => n.grepLog(V4_LINE).map(l => tail(l, 'v4'))[0] || null);
|
|
const v3SwitchLines = nodes.map(n => n.grepLog(V3_LINE).map(l => tail(l, 'v3'))[0] || null);
|
|
const sinks = dag.map(d => String(d.sink || '?').slice(0, 16));
|
|
const counts = dag.map(d => d.blockCount ?? '?');
|
|
const tips = dag.map(d => (d.tipHashes || []).length);
|
|
|
|
// the Metal miner's protocol lines (gate G4b): PREPARE sent (the miner), prepared / need / error (the worker), the swap
|
|
let metal = null;
|
|
if (METAL) {
|
|
const L = minerLog(0);
|
|
const t = (l) => { const m = /^(\d+\.\d+) /.exec(l); return m ? +m[1] * 1000 : null; };
|
|
const switchAt = firstV4 ? t0 + firstV4.at * 1000 : null;
|
|
const prepares = L.filter(l => /PREPARE sent for epoch seed/.test(l)).map(l => l.replace(/^\S+ /, ''));
|
|
const prepared = L.filter(l => /worker: prepared /.test(l)).map(l => l.replace(/^\S+ /, ''));
|
|
const preparedV4 = prepared.filter(l => / class v4 /.test(l));
|
|
const need = L.filter(l => /worker: need |^\S+ worker error:.*need /.test(l) || /\bneed [0-9a-f]{64}/.test(l));
|
|
// a PACK OUT OF DATE before the first class v4 prepare is the v2/v3 rebuild path at work (a seed that flipped inside
|
|
// the quarter-lead confirm window, 3 DAA on the 60x profile); the gate judges the v4 path from its first prepare on
|
|
const firstV4Prepare = L.findIndex(l => /PREPARE sent for epoch seed .* class v4/.test(l));
|
|
const sinceV4 = (l, i) => firstV4Prepare < 0 || i >= firstV4Prepare;
|
|
const mismatch = L.filter((l, i) => sinceV4(l, i) && /program class mismatch|era seed mismatch|PACK OUT OF DATE|pack .*: program pack/.test(l));
|
|
const mismatchBefore = L.filter((l, i) => !sinceV4(l, i) && /program class mismatch|era seed mismatch|PACK OUT OF DATE/.test(l));
|
|
const refused = L.filter(l => /exiting with code 4[24]|refused the program pack|prepare-failed/.test(l));
|
|
const swaps = L.filter(l => /swapped with no pause|compiles inline|worker without prepare support/.test(l)).map(l => l.replace(/^\S+ /, ''));
|
|
const accepted = L.filter(l => /ACCEPTED block/.test(l));
|
|
const acceptedAfter = switchAt ? accepted.filter(l => (t(l) || 0) >= switchAt).length : 0;
|
|
const found = L.filter(l => /worker: found |^\S+ found /.test(l)).length;
|
|
const status = L.filter(l => /miner 'metal0' \[igneum-pow\]/.test(l)).map(l => l.replace(/^\S+ /, ''));
|
|
const lastStatus = status.at(-1) || '';
|
|
const mismatched = +(/mismatched=(\d+)/.exec(lastStatus)?.[1] ?? 0);
|
|
const rate = /hash=([\d.]+) MH\/s/.exec(lastStatus)?.[1];
|
|
metal = { worker: METAL, prepares, prepared, prepared_v4: preparedV4, prepared_v3: prepared.filter(l => / class v3 /.test(l)), need: need.length, mismatch_lines: mismatch, rebuild_lines_before_v4: mismatchBefore, refused, swaps, accepted_total: accepted.length, accepted_after_switch: acceptedAfter, found_lines: found, cpu_recheck_mismatched: mismatched, rate_mh_s: rate, last_status: lastStatus };
|
|
}
|
|
|
|
const checks = {
|
|
v4_switch_line_on_every_node: switchLines.every(Boolean),
|
|
v4_switch_line_names_the_rounded_epoch: switchLines.every(l => l && FIRST_V4_EPOCH != null && l.includes(`active from epoch ${FIRST_V4_EPOCH} `)),
|
|
v3_switch_line_on_every_node: v3SwitchLines.every(Boolean),
|
|
template_switched_at_the_first_v4_epoch: firstV4 != null && firstV4.epoch === FIRST_V4_EPOCH,
|
|
template_switched_at_the_first_v3_epoch: FIRST_V3_EPOCH == null ? firstV3 == null : (firstV3 != null && firstV3.epoch === FIRST_V3_EPOCH),
|
|
blocks_before_the_boundary: before.length > 0,
|
|
blocks_after_the_boundary: after.length > 0,
|
|
v3_and_v4_programs_seen: v3Ids.length > 0 && v4Ids.length > 0,
|
|
v2_programs_seen_when_v3_starts_later: FIRST_V3_EPOCH == null || FIRST_V3_EPOCH === 0 || v2Ids.length > 0,
|
|
program_ids_differ_across_the_switch: v4Ids.length > 0 && !olderIds.some(id => v4Ids.includes(id)),
|
|
miners_agree_on_every_program: programRows.every(p => !p.disagree),
|
|
zero_rejected_by_miners: rejectedMiner.every(r => r.length === 0),
|
|
zero_rejected_by_nodes: rejectedNode.every(r => r.length === 0),
|
|
sinks_agree: new Set(sinks).size === 1,
|
|
block_counts_agree: new Set(counts.map(String)).size === 1,
|
|
...(METAL ? {
|
|
metal_prepare_sent_for_v4: metal.prepares.some(l => / class v4 /.test(l)),
|
|
metal_worker_prepared_v4_pack: metal.prepared_v4.length > 0,
|
|
metal_no_need_or_mismatch: metal.need === 0 && metal.mismatch_lines.length === 0 && metal.refused.length === 0,
|
|
metal_accepted_blocks_after_switch: metal.accepted_after_switch > 0,
|
|
metal_cpu_recheck_clean: metal.cpu_recheck_mismatched === 0,
|
|
metal_swapped_without_pause: metal.swaps.some(l => /swapped with no pause/.test(l)),
|
|
} : {}),
|
|
};
|
|
const pass = Object.values(checks).every(Boolean);
|
|
const summary = {
|
|
pass, checks, activation: ACTIVATION ?? 'never', v3_activation: V3_ACTIVATION ?? 'never', epoch_blocks: EPOCH, first_v3_epoch: FIRST_V3_EPOCH, first_v4_epoch: FIRST_V4_EPOCH, boundary_daa: Number.isFinite(BOUNDARY) ? BOUNDARY : 'never', secs: SECS, threads: THREADS,
|
|
node: IGNEUMD, miner: CPU_MINER, metal_worker: METAL, genesis_bits: `0x${GENESIS_BITS.toString(16)}`,
|
|
template_switch_v3: firstV3, template_switch: firstV4, run_ended_at_s: switchEnd, final_daa: lastDaa,
|
|
blocks: { total: blocks.length, before_boundary: before.length, after_boundary: after.length, chain_before: chainBefore, chain_after: chainAfter },
|
|
programs: programRows, accepted_per_miner: accepted,
|
|
rejected_by_miners: rejectedMiner.map(r => r.length), rejected_by_nodes: rejectedNode.map(r => r.length),
|
|
rejected_lines: [...rejectedMiner.flat(), ...rejectedNode.flat()].slice(0, 20),
|
|
sinks, block_counts: counts, tips_per_node: tips, switch_lines: switchLines, v3_switch_lines: v3SwitchLines, samples, metal,
|
|
};
|
|
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
|
|
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'}: ${firstV4 ? `v4 from epoch ${firstV4.epoch} at DAA ${firstV4.daa}` : 'NO v4 epoch seen'}; blocks ${before.length} before / ${after.length} after the boundary at DAA ${Number.isFinite(BOUNDARY) ? BOUNDARY : 'never'} (chain ${chainBefore} / ${chainAfter}); programs ${programRows.map(p => `e${p.epoch}:${p.class}:${p.program_id}:${Math.max(...p.ready_ms)}ms`).join(' ')}; rejected miners ${rejectedMiner.map(r => r.length).join('/')} nodes ${rejectedNode.map(r => r.length).join('/')}; sinks ${sinks.join(' ')} (${checks.sinks_agree ? 'agree' : 'DIFFER'}); block counts ${counts.join('/')}; v4 switch lines ${switchLines.filter(Boolean).length}/3, v3 switch lines ${v3SwitchLines.filter(Boolean).length}/3`);
|
|
if (metal) {
|
|
log(`METAL: ${metal.prepares.length} PREPARE lines (${metal.prepares.filter(l => / class v4 /.test(l)).length} class v4, ${metal.prepares.filter(l => / class v3 /.test(l)).length} class v3), ${metal.prepared.length} prepared (${metal.prepared_v4.length} v4, ${metal.prepared_v3.length} v3), need ${metal.need}, mismatch/refusal lines ${metal.mismatch_lines.length + metal.refused.length}, accepted ${metal.accepted_total} (${metal.accepted_after_switch} after the switch), cpu re-check mismatched ${metal.cpu_recheck_mismatched}, rate ${metal.rate_mh_s} MH/s`);
|
|
for (const l of [...metal.prepares, ...metal.prepared, ...metal.swaps]) log(` ${l.slice(0, 260)}`);
|
|
for (const l of [...metal.mismatch_lines, ...metal.refused].slice(0, 10)) log(` BAD ${l.slice(0, 260)}`);
|
|
}
|
|
for (const [k, v] of Object.entries(checks)) if (!v) log(`FAILED CHECK ${k}`);
|
|
log(`summary: ${TMP}/summary.json`);
|
|
await stopAll();
|
|
process.exit(pass ? 0 : 1);
|