289 lines
22 KiB
JavaScript
289 lines
22 KiB
JavaScript
#!/usr/bin/env node
|
|
// ATTACK-PASS COPY (F10, 7 October 2026) of infra/fast-time/latency-ladder.mjs from branch ladder at 7003f9f5, taken
|
|
// verbatim except: (1) ROOT is found by walking up from this file to the tree that holds tools/finality-attacks/lib/rpc.mjs
|
|
// (the file lives at tools/attack/f10-ladder/ on the Mac and at attack-f10/ on the box), the two lib imports are dynamic;
|
|
// (2) the override file is this directory's copy of the ladder branch's override-60x.json (the attack-pass tree's copy
|
|
// lacks the latency_ladder fields); (3) ports, devnet suffix and data dir come from IGNEUM_F10_BASE / IGNEUM_F10_SUFFIX /
|
|
// IGNEUM_LADDER_TMP with the F10 defaults 29900 / 990 / /tmp/igneum-fast-time-attack-f10, so this network never collides
|
|
// with the ladder lane's (29720 / 972) or F7's (29800 / 980); (4) the binaries default to the box's ladder lane layout,
|
|
// read-only. The checks, the cases and the known-failed case are the original's, unchanged.
|
|
//
|
|
// The latency ladder's fast-time gate (docs/design/latency-ladder.md section 9; the class-v4-signal.mjs shape): a 3-node
|
|
// network on override-60x.json, class v4 from genesis (v3 and the v4 floor at 0, the class window 0: class signalling off,
|
|
// so the ladder opens the header's high byte on its own), the ladder active from DAA 0 with one window of --window DAA
|
|
// (default 60, one epoch; seven windows = 420 DAA, so the first epoch whose seed block has seven full windows below it is
|
|
// epoch 8 at DAA 480), each node's ladder signal set by IGNEUM_LADDER_SIGNAL (--signal a,b,c of up|down|none), one real CPU
|
|
// miner per node. Ports 29720 and up, network igneum-devnet-972, data /tmp/igneum-fast-time-ladder.
|
|
//
|
|
// The cases and the known-failed case:
|
|
// --signal up,up,none --expect no-step two of three miners signal up: about 67 percent, rung 0 must hold (run 10 epochs)
|
|
// --signal up,up,up --expect step all three: rung 1 (35 shadow passes) from epoch 8, the first with seven full
|
|
// windows, every miner's rung-1 program id equal to the CLI's --shadow-reps 35 id and
|
|
// unequal to the rung-0 id, and NO second step inside the next two epochs
|
|
// --signal up,up,none --expect step the known-failed case: the harness must report FAIL (no step happened)
|
|
//
|
|
// node infra/fast-time/latency-ladder.mjs --signal a,b,c --expect step|no-step [--window 60] [--secs 900] [--epochs 10]
|
|
// IGNEUMD, IGNEUM_MINER, IGNEUM_POW name the binaries (defaults: the ladder fork worktree's target/release and
|
|
// igneum-pow/target/release/igneum-pow, the layout on igneum-build-1 under /srv/builds/igneum-wt-ladder).
|
|
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
|
|
|
|
import { dirname } from 'node:path';
|
|
const HERE = dirname(new URL(import.meta.url).pathname) + '/';
|
|
function findRoot(from) { let d = from; for (let i = 0; i < 6; i++) { if (existsSync(`${d}tools/finality-attacks/lib/rpc.mjs`)) return d; d = dirname(d.replace(/\/$/, '')) + '/'; } throw new Error('no tree with tools/finality-attacks/lib/rpc.mjs above ' + from); }
|
|
const ROOT = process.env.IGNEUM_ROOT ? process.env.IGNEUM_ROOT.replace(/\/?$/, '/') : findRoot(HERE);
|
|
const { connectRpc } = await import(`${ROOT}tools/finality-attacks/lib/rpc.mjs`);
|
|
const { devAddress } = await import(`${ROOT}tools/harness/lib/address.mjs`);
|
|
const FILE = `${HERE}override-60x.json`;
|
|
const BIN = process.env.IGNEUM_LADDER_BIN || '/srv/builds/igneum-wt-ladder/vendor/igneum-node-ladder/target/release';
|
|
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
|
|
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
|
|
const IGNEUM_POW = process.env.IGNEUM_POW || '/srv/builds/igneum-wt-ladder/igneum-pow/target/release/igneum-pow';
|
|
const TMP = process.env.IGNEUM_LADDER_TMP || '/tmp/igneum-fast-time-attack-f10';
|
|
const BASE = +(process.env.IGNEUM_F10_BASE || 29900), SUFFIX = +(process.env.IGNEUM_F10_SUFFIX || 990);
|
|
const NEVER = '18446744073709551615';
|
|
const RUNG0 = 27, RUNG1 = 35, WINDOWS = 7, THRESHOLD = 9000;
|
|
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; };
|
|
const GENESIS_BITS = flag('genesis-bits', 0x1f010000);
|
|
const SECS = flag('secs', 900);
|
|
const EPOCHS = flag('epochs', 10);
|
|
const WINDOW = flag('window', 60);
|
|
const SIGNAL = (sflag('signal') || 'up,up,up').split(',').map(s => s.trim().toLowerCase());
|
|
const EXPECT = sflag('expect') || 'step';
|
|
if (!['step', 'no-step'].includes(EXPECT) || SIGNAL.length !== 3 || !SIGNAL.every(s => ['up', 'down', 'none'].includes(s))) { console.error('usage: --signal a,b,c (up|down|none) --expect step|no-step'); process.exit(2); }
|
|
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]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
|
|
|
|
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
|
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 LEAD = field('pow_epoch_lead');
|
|
const DAY_MS = field('pow_day_ms');
|
|
// the first epoch whose seed block (the last chain block below L*e - lead) can have DAA >= 7 x WINDOW: L*e - lead - 1 >= 7W
|
|
let FIRST_STEP_EPOCH = 0;
|
|
while (FIRST_STEP_EPOCH * EPOCH - LEAD - 1 < WINDOWS * WINDOW) FIRST_STEP_EPOCH++;
|
|
function mergeOverrideText(text, fields) {
|
|
let out = text;
|
|
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
|
|
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`);
|
|
}
|
|
const override = `${TMP}/override.json`;
|
|
writeFileSync(override, mergeOverrideText(baseText, {
|
|
genesis_bits: GENESIS_BITS, skip_proof_of_work: false,
|
|
program_class_v3_activation_daa: '0', program_class_v4_activation_daa: '0', program_class_v4_signal_window_daa: '0',
|
|
latency_ladder_activation_daa: '0', latency_ladder_window_daa: String(WINDOW),
|
|
}));
|
|
log(`signals ${SIGNAL.join('/')}, expect ${EXPECT}; class v4 from genesis, the ladder active from DAA 0, window ${WINDOW} DAA x ${WINDOWS} (the first epoch that can step is ${FIRST_STEP_EPOCH}, DAA ${FIRST_STEP_EPOCH * EPOCH}); ${EPOCH} DAA per epoch, lead ${LEAD}; run ${SECS} s or ${EPOCHS} epochs`);
|
|
|
|
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');
|
|
// the node's own ladder signal: what its templates carry in bits 15 and 14
|
|
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out], env: { ...process.env, IGNEUM_LADDER_SIGNAL: SIGNAL[this.i] } });
|
|
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}, signals ${SIGNAL[this.i]}`);
|
|
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); });
|
|
const minerLog = (i) => { try { return readFileSync(`${TMP}/cpu${i}.log`, 'utf8').split('\n'); } catch { return []; } };
|
|
const STEP_LINE = /Latency ladder step by miner signal: epoch (\d+) moves to rung (\d+) \((\d+) shadow passes, from rung (\d+)\): (up|down) in each of (\d+) consecutive windows of (\d+) DAA .*weakest up (\d+) bps, weakest down (\d+) bps/;
|
|
const LADDER_LINE = /Latency ladder from the override file/;
|
|
const ACTIVE_LINE = /Latency ladder active: rungs/;
|
|
const OWN_LINE = /Latency ladder signal from IGNEUM_LADDER_SIGNAL: this node signals (\w+)/;
|
|
|
|
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();
|
|
const nodes = [n0, n1, n2];
|
|
for (const n of nodes) log(`n${n.i}: ${n.grepLog(LADDER_LINE).map(l => l.replace(/^.*?(Latency ladder from)/, '$1'))[0] || '(no ladder line)'} | ${n.grepLog(OWN_LINE).map(l => l.replace(/^.*?(this node signals)/, '$1'))[0] || '(no signal line)'}`);
|
|
log(`n0 digest: ${n0.grepLog(/Consensus params digest/).map(l => l.replace(/^.*?digest: /, '').slice(0, 16)).join(' ')}`);
|
|
nodes.forEach((n, i) => miner(CPU_MINER, ['mine', n.grpc, '1', 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-ladder');
|
|
|
|
const epochs = new Map();
|
|
let firstStep = null, lastEpoch = -1, lastReport = 0, lastDaa = 0, endAt = null;
|
|
const samples = [];
|
|
while (Date.now() - t0 < SECS * 1000) {
|
|
await sleep(1000);
|
|
let daa = null, epoch = null, cls = null, reps = null, nextReps = null, step = null, nextStep = null, up = null, upWeak = null, down = null, sig = null, stepEpoch = null, eraSeed = 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; eraSeed = pe.eraSeed;
|
|
reps = pe.latencyLadderReps; nextReps = pe.nextLatencyLadderReps; step = pe.latencyLadderStep; nextStep = pe.nextLatencyLadderStep;
|
|
up = pe.latencyLadderUpBps; upWeak = pe.latencyLadderUpWeakestBps; down = pe.latencyLadderDownBps; sig = pe.latencyLadderSignal; stepEpoch = pe.latencyLadderStepEpoch;
|
|
} catch (e) { log(`template: ${e.message}`); }
|
|
if (epoch != null && epoch !== lastEpoch) {
|
|
epochs.set(epoch, { class: cls, reps, step, firstSeenDaa: daa, at: +since(), eraSeed: eraSeed == null ? null : String(eraSeed), up_bps: up, up_weakest_bps: upWeak, down_bps: down, step_epoch: stepEpoch ?? null });
|
|
log(`epoch ${lastEpoch} -> ${epoch} at daa ${daa}, ${since()} s: template class ${cls} rung ${step} (${reps} passes), next rung ${nextStep} (${nextReps}), up ${up} bps (weakest of ${WINDOWS}: ${upWeak}), down ${down} bps, this node signals ${sig}, step took effect at epoch ${stepEpoch ?? 'none'}`);
|
|
if (firstStep == null && step > 0) { firstStep = { epoch, daa, step, reps, at: +since() }; log(`LADDER STEP: the template is rung ${step} (${reps} shadow passes) 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} rung ${step} (${reps}) up ${up} bps weakest ${upWeak} blocks/sink per node ${counts.join(' ')}`);
|
|
samples.push({ t: +since(), daa, epoch, step, reps, up_bps: up, up_weakest_bps: upWeak, nodes: counts });
|
|
}
|
|
// the end: two epochs after a step (to show no second step), or --epochs epochs when no step is expected
|
|
if (firstStep != null && daa != null && daa >= (firstStep.epoch + 2) * EPOCH + LEAD) { endAt = +since(); break; }
|
|
if (firstStep == null && daa != null && daa >= EPOCHS * EPOCH) { endAt = +since(); break; }
|
|
}
|
|
await sleep(3000);
|
|
|
|
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;
|
|
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, version: +b.header.version, chain: !!b.verboseData?.isChainBlock }); 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 BOUNDARY = firstStep ? firstStep.epoch * EPOCH : Infinity;
|
|
const before = blocks.filter(b => b.daa < BOUNDARY), after = blocks.filter(b => b.daa >= BOUNDARY);
|
|
// the ladder bits on the chain: bit 15 up, bit 14 down; the object byte (bits 8 to 13) must be 0 (class signalling off)
|
|
const bitsOf = (v) => (v & 0x8000) ? 'up' : (v & 0x4000) ? 'down' : 'none';
|
|
const ladderBits = blocks.reduce((m, b) => { const k = bitsOf(b.version); m[k] = (m[k] || 0) + 1; return m; }, {});
|
|
const upShareOnChain = blocks.length ? Math.round(10000 * (blocks.filter(b => bitsOf(b.version) === 'up').length) / blocks.length) : 0;
|
|
const objectBytes = blocks.reduce((m, b) => { const v = (b.version >> 8) & 0x3f; m[v] = (m[v] || 0) + 1; return m; }, {});
|
|
// genesis carries header version 0 (genesis.rs), every mined block the block version 2 in its low byte
|
|
const lowBytes = new Set(blocks.filter(b => b.daa > 0).map(b => b.version & 0xff));
|
|
|
|
const programs = new Map();
|
|
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() };
|
|
if (e.id !== m[6] || e.class !== m[5]) e.disagree = true;
|
|
e.miners.add(i); programs.set(k, e);
|
|
}
|
|
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 }));
|
|
function cliId(seedHex, eraHex, reps) {
|
|
if (!existsSync(IGNEUM_POW)) return null;
|
|
const r = spawnSync(IGNEUM_POW, ['show', '--epoch-hex', seedHex, '--program-class', 'v4', '--era-hex', eraHex, '--shadow-reps', String(reps)], { encoding: 'utf8' });
|
|
const m = /program id ([0-9a-f]{16})/.exec(r.stdout || '');
|
|
return m ? m[1] : null;
|
|
}
|
|
const idRows = [];
|
|
for (const [k, e] of programs) {
|
|
const ep = epochs.get(e.epoch);
|
|
if (!ep || ep.eraSeed == null) continue;
|
|
const reps = ep.reps ?? 0;
|
|
idRows.push({ epoch: e.epoch, seed: e.seed, reps, miners_id: e.id, miners: e.miners.size, cli_rung0: cliId(k, ep.eraSeed, 0), cli_at_reps: cliId(k, ep.eraSeed, reps) });
|
|
}
|
|
const steppedRows = idRows.filter(r => r.reps !== RUNG0 && r.reps !== 0);
|
|
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 stepLines = nodes.map(n => n.grepLog(STEP_LINE).map(l => l.replace(/^.*?(Latency ladder step by miner signal)/, '$1')));
|
|
const firstStepLine = stepLines.map(ls => ls[0] || null);
|
|
const stepEpochs = firstStepLine.map(l => { const m = l && STEP_LINE.exec(l); return m ? +m[1] : null; });
|
|
const stepRungs = firstStepLine.map(l => { const m = l && STEP_LINE.exec(l); return m ? +m[2] : null; });
|
|
const stepWeakestUp = firstStepLine.map(l => { const m = l && STEP_LINE.exec(l); return m ? +m[8] : null; });
|
|
const sinks = dag.map(d => String(d.sink || '?').slice(0, 16));
|
|
const counts = dag.map(d => d.blockCount ?? '?');
|
|
const maxEpochSeen = Math.max(-1, ...epochs.keys());
|
|
const repsSeen = [...epochs.values()].map(e => e.reps);
|
|
const afterStep = firstStep ? [...epochs.entries()].filter(([e]) => e > firstStep.epoch).map(([, v]) => v.step) : [];
|
|
|
|
const common = {
|
|
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,
|
|
miners_agree_on_every_program: programRows.every(p => !p.disagree),
|
|
ladder_line_on_every_node: nodes.every(n => n.grepLog(LADDER_LINE).length > 0 && n.grepLog(ACTIVE_LINE).length > 0),
|
|
every_node_signals_its_bits: nodes.every((n, i) => n.grepLog(OWN_LINE).some(l => OWN_LINE.exec(l)[1] === SIGNAL[i])),
|
|
every_epoch_class_v4: [...epochs.values()].every(e => e.class === 4),
|
|
rung0_ids_equal_the_cli_rung0_id: idRows.filter(r => r.reps === RUNG0).length > 0 && idRows.filter(r => r.reps === RUNG0).every(r => r.cli_rung0 != null && r.cli_rung0 === r.miners_id),
|
|
};
|
|
// every block carries block version 2, an object byte of 0 (class signalling off) and the ladder bits of one of the three
|
|
// nodes; genesis, made before any node, is the one bit-less block when every node signals (the first run of the known-failed
|
|
// case, 22:26Z, failed this check on genesis's version 0 in the low-byte test, a harness fault, not a chain one)
|
|
const noneNodes = SIGNAL.filter(s => s === 'none').length;
|
|
common.chain_carries_the_bits = blocks.length > 0 && [...lowBytes].every(v => v === 2) && Object.keys(objectBytes).every(v => +v === 0)
|
|
&& Object.keys(ladderBits).every(k => SIGNAL.includes(k) || k === 'none') && (noneNodes > 0 || (ladderBits.none || 0) === 1);
|
|
let checks;
|
|
if (EXPECT === 'step') {
|
|
checks = {
|
|
...common,
|
|
template_stepped_to_rung_1: firstStep != null && firstStep.step === 1 && firstStep.reps === RUNG1,
|
|
stepped_at_the_first_full_window_epoch: firstStep != null && firstStep.epoch === FIRST_STEP_EPOCH,
|
|
step_line_on_every_node_same_epoch: stepEpochs.every(e => e != null) && new Set(stepEpochs).size === 1 && stepEpochs[0] === (firstStep && firstStep.epoch) && stepRungs.every(r => r === 1),
|
|
weakest_up_at_or_above_threshold: stepWeakestUp.every(s => s != null && s >= THRESHOLD),
|
|
no_second_step_inside_seven_windows: firstStep != null && afterStep.length >= 2 && afterStep.every(s => s === 1) && stepLines.every(ls => ls.length === 1),
|
|
blocks_on_both_sides: before.length > 0 && after.length > 0,
|
|
rung1_ids_equal_the_cli_rung1_id: steppedRows.length > 0 && steppedRows.every(r => r.reps === RUNG1 && r.cli_at_reps != null && r.cli_at_reps === r.miners_id && r.miners === 3),
|
|
rung1_ids_differ_from_the_same_seed_rung0_id: steppedRows.length > 0 && steppedRows.every(r => r.cli_rung0 != null && r.cli_rung0 !== r.miners_id),
|
|
};
|
|
} else {
|
|
checks = {
|
|
...common,
|
|
template_never_above_rung_0: firstStep == null && repsSeen.every(r => r === RUNG0 || r === 0),
|
|
no_step_line_on_any_node: stepLines.every(ls => ls.length === 0),
|
|
ran_the_epochs: maxEpochSeen >= EPOCHS - 1,
|
|
passed_the_first_full_window_epoch: maxEpochSeen >= FIRST_STEP_EPOCH,
|
|
up_share_under_threshold_on_chain: upShareOnChain < THRESHOLD,
|
|
};
|
|
}
|
|
const pass = Object.values(checks).every(Boolean);
|
|
const summary = {
|
|
pass, expect: EXPECT, signals: SIGNAL, checks, window: WINDOW, windows: WINDOWS, threshold_bps: THRESHOLD, epoch_blocks: EPOCH, lead: LEAD, first_step_epoch: FIRST_STEP_EPOCH,
|
|
node: IGNEUMD, miner: CPU_MINER, pow: IGNEUM_POW, template_step: firstStep, run_ended_at_s: endAt, final_daa: lastDaa, max_epoch_seen: maxEpochSeen,
|
|
epochs: Object.fromEntries([...epochs.entries()].map(([k, v]) => [k, v])),
|
|
blocks: { total: blocks.length, before_boundary: before.length, after_boundary: after.length, ladder_bits: ladderBits, object_bytes: objectBytes, up_share_bps_on_chain: upShareOnChain },
|
|
programs: programRows, program_id_rows: idRows, accepted_per_miner: accepted,
|
|
rejected_by_miners: rejectedMiner.map(r => r.length), rejected_by_nodes: rejectedNode.map(r => r.length),
|
|
sinks, block_counts: counts, step_lines: stepLines, samples,
|
|
};
|
|
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
|
|
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'} (expect ${EXPECT}, signals ${SIGNAL.join('/')}): ${firstStep ? `rung ${firstStep.step} (${firstStep.reps} passes) from epoch ${firstStep.epoch} at DAA ${firstStep.daa}` : 'no step'}; epochs seen ${[...epochs.entries()].map(([e, v]) => `e${e}:r${v.step}:${v.up_weakest_bps}bps`).join(' ')}; chain bits ${JSON.stringify(ladderBits)} (${upShareOnChain} bps up); blocks ${before.length} / ${after.length}; rejected miners ${rejectedMiner.map(r => r.length).join('/')} nodes ${rejectedNode.map(r => r.length).join('/')}; sinks ${sinks.join(' ')} at ${counts.join('/')}`);
|
|
for (const r of idRows) log(`PROGRAM ID epoch ${r.epoch} seed ${r.seed} reps ${r.reps}: miners ${r.miners_id} (${r.miners} of 3) cli at reps ${r.cli_at_reps} cli rung 0 ${r.cli_rung0}`);
|
|
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);
|