The same harness at 10, 20 and 30 s of propagation at 1 bps: at 10 s one shared DAG (254 blocks via relay, mergeset 3.4, red 36 percent, the controller within 1.1 percent); at 20 s under-merging (mergeset 1.6, the rate 19 percent under, the difficulty 40 percent under); at 30 s nothing merges (mergeset 1.0, every node its own chain). At k = 18 the blocks in flight pass k between 10 and 20 s, k's own derivation. Fix shape, not built: an IBD trigger on a stale sink (a node whose relayed blocks stop merging syncs the heavier chain instead of mining alone; the 30-s hub never entered IBD) and k with the merge depth sized to the measured delay bound at the block rate. Owed: the long run's decay (3 via relay in 40 minutes) with a kept log; the harness now takes --loglevel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
294 lines
20 KiB
JavaScript
294 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
|
// Horizon lane 5, proposal 2 (docs/analysis/horizon/network.md 5.4 and 6): the difficulty controller against a
|
|
// relay-delayed DAG. Three nodes in a star, the two spokes reaching the hub through a TCP proxy that holds every byte
|
|
// for --delay-ms in each direction (the handshake's first --handshake-ms pass undelayed: the p2p version exchange
|
|
// times out at 4 s, protocol/p2p/src/handshake.rs), one real CPU miner per node, override-60x.json re-rated to --bps.
|
|
// A block crosses a link in three legs (inv, request, block: Kaspa's relay flow), so the document's propagation
|
|
// delay d (--propagation-ms, 3,000 or 30,000) is one third per leg: --delay-ms defaults to --propagation-ms / 3.
|
|
//
|
|
// The claim under test: under rule v3 (difficulty_v3_activation_daa 0: every chain step counts every merged block's
|
|
// work over the real span, the clock cap and lag bound in DAA seconds, m x 20 s per merging step) the DAG rate holds
|
|
// the target and the difficulty tracks the miners' hash within 10 percent after --measure-from seconds; under the
|
|
// live rule (v2: blue work over a 20 T capped step) a 3-s delay at 10 bps settles near 6.67 blocks/s with the
|
|
// difficulty 1.5x the hash-implied value (controller.py), which is this harness's known-failed case.
|
|
//
|
|
// The cases (each --secs 900, measured from --measure-from 600):
|
|
// --bps 10 --propagation-ms 3000 --rule v3 --expect pass the proposal's first case
|
|
// --bps 1 --propagation-ms 30000 --rule v3 --expect pass the proposal's second case
|
|
// --bps 10 --propagation-ms 3000 --rule v2 --expect fail the known-failed case: the harness must report FAIL
|
|
//
|
|
// Pass = |DAG rate / target - 1| <= 0.10 over the measured window AND |expected hashes per block / (hash x T) - 1|
|
|
// <= 0.10 averaged over the window, where expected hashes per block come from the sink's bits (2^256 / (target + 1))
|
|
// and hash is the sum of the three miners' own `hash=... MH/s` status lines (every 30 s). The red share, the chain
|
|
// step spacing and the three nodes' block counts are reported, not judged.
|
|
//
|
|
// node infra/fast-time/difficulty-v3.mjs --bps 10 --propagation-ms 3000 --rule v3 --expect pass [--secs 900] [--delay-ms <per leg>]
|
|
// [--measure-from 600] [--genesis-bits 0x1f080000] [--handshake-ms 5000] [--out docs/plans/counter-asic-3-gate/difficulty-v3-<case>.json]
|
|
// IGNEUMD, IGNEUM_MINER name the binaries (defaults: vendor/igneum-node-0316/target-0316/release).
|
|
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
import { createServer, connect as netConnect } from 'node:net';
|
|
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_0316_BIN || `${ROOT}vendor/igneum-node-0316/target-0316/release`;
|
|
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
|
|
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
|
|
const TMP = '/tmp/igneum-fast-time-dv3';
|
|
const BASE = 29790, SUFFIX = 979;
|
|
const NEVER = '18446744073709551615';
|
|
const args = process.argv.slice(2);
|
|
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? Number(args[i + 1]) : dflt; };
|
|
const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
|
|
const BPS = flag('bps', 10);
|
|
const PROPAGATION_MS = flag('propagation-ms', 3000);
|
|
const DELAY_MS = flag('delay-ms', Math.round(PROPAGATION_MS / 3));
|
|
const RULE = sflag('rule') || 'v3';
|
|
// --pipeline <n>: IGNEUM_RELAY_PIPELINE for every node (0.3.18, ledger N3): how many relay blocks one peer may have in
|
|
// flight; 1 is the 0.3.17 one-at-a-time flow, the known-failed shape of the relay ceiling; absent = the node's default (16)
|
|
const PIPELINE = sflag('pipeline');
|
|
// --loglevel debug: the nodes' log level (default info); the 30-s reading (7 October 2026) needs the orphan, locator and IBD lines
|
|
const LOGLEVEL = sflag('loglevel') || 'info';
|
|
const EXPECT = sflag('expect') || 'pass';
|
|
const SECS = flag('secs', 900);
|
|
const MEASURE_FROM = flag('measure-from', 600);
|
|
const HANDSHAKE_MS = flag('handshake-ms', 5000);
|
|
const GENESIS_BITS = flag('genesis-bits', BPS >= 10 ? 0x1f080000 : 0x1f010000); // 2^13 hashes per block at 10 bps, 2^16 at 1 bps (mantissa x 2^224)
|
|
const CASE = sflag('case') || `${BPS}bps-d${PROPAGATION_MS}ms-${RULE}${PIPELINE != null ? `-pipe${PIPELINE}` : ''}`;
|
|
const OUT = sflag('out') || `${ROOT}docs/plans/counter-asic-3-gate/difficulty-v3-${CASE}.json`;
|
|
if (!['v2', 'v3'].includes(RULE) || !['pass', 'fail'].includes(EXPECT) || ![1, 10].includes(BPS)) { console.error('usage: --bps 1|10 --delay-ms <ms> --rule v2|v3 --expect pass|fail'); process.exit(2); }
|
|
const T_MS = 1000 / BPS;
|
|
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); }
|
|
|
|
try { spawnSync('pkill', ['-f', `devnet-suffix=${SUFFIX}`]); } catch { }
|
|
await sleep(1500);
|
|
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
|
|
|
// The override file is merged as TEXT (a never height is beyond a JavaScript number; class-v4.mjs has the history).
|
|
const baseText = readFileSync(FILE, 'utf8');
|
|
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
|
|
export 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`);
|
|
}
|
|
// The block-rate block (the file's is 1 bps). At 10 bps: docs/override-params.md's 10 bps example for k, parents,
|
|
// mergeset limit and the sample rates; the depths are the file's x10 (merge 600, finality 7,200, pruning 138,380,
|
|
// above the Prunality bound 2F + 4 M k + 2k + 2 = 137,658); the PoW epoch 600 DAA with lead 100 so an epoch stays
|
|
// 60 s of wall time and the CPU miners regenerate their program as often as at 1 bps.
|
|
export function rerateBlockrate(text, bps) {
|
|
if (bps === 1) return text;
|
|
const block = `"blockrate": {
|
|
"target_time_per_block": 100,
|
|
"ghostdag_k": 124,
|
|
"past_median_time_sample_rate": 10,
|
|
"difficulty_sample_rate": 2,
|
|
"max_block_parents": 16,
|
|
"mergeset_size_limit": 248,
|
|
"merge_depth": 600,
|
|
"finality_depth": 7200,
|
|
"pruning_depth": 138380,
|
|
"coinbase_maturity": 20
|
|
}`;
|
|
return text.replace(/"blockrate":\s*\{[^}]*\}/, block);
|
|
}
|
|
const EPOCH = BPS === 1 ? field('pow_epoch_blocks') : 600;
|
|
const LEAD = BPS === 1 ? field('pow_epoch_lead') : 100;
|
|
const DAY_MS = field('pow_day_ms');
|
|
const override = `${TMP}/override.json`;
|
|
writeFileSync(override, mergeOverrideText(rerateBlockrate(baseText, BPS), {
|
|
genesis_bits: GENESIS_BITS, skip_proof_of_work: false, pow_epoch_blocks: EPOCH, pow_epoch_lead: LEAD,
|
|
difficulty_v2_activation_daa: '0', difficulty_v3_activation_daa: RULE === 'v3' ? '0' : NEVER,
|
|
program_class_v3_activation_daa: NEVER, program_class_v4_activation_daa: NEVER,
|
|
}));
|
|
log(`case ${CASE}: ${BPS} bps (T ${T_MS} ms), propagation ${PROPAGATION_MS} ms as a proxy delay of ${DELAY_MS} ms per leg each way (handshake ${HANDSHAKE_MS} ms undelayed), rule ${RULE}, genesis bits 0x${GENESIS_BITS.toString(16)} (${expectedHashes(GENESIS_BITS).toExponential(3)} hashes per block), ${SECS} s measured from ${MEASURE_FROM} s, expect ${EXPECT}`);
|
|
|
|
// ---- the delay proxy ----
|
|
// Every chunk in each direction is written to the other side DELAY_MS after it arrived (timers of one delay fire in
|
|
// order, so the byte order holds); the first HANDSHAKE_MS of a connection pass undelayed. Kaspa's flows wait up to
|
|
// 120 s (protocol/p2p/src/common.rs DEFAULT_TIMEOUT), so a 30-s delay (60-s round trip) fits inside them.
|
|
class DelayProxy {
|
|
constructor(listenPort, targetPort) { this.listenPort = listenPort; this.targetPort = targetPort; this.conns = 0; this.bytes = 0; }
|
|
start() {
|
|
return new Promise((resolve) => {
|
|
this.server = createServer((client) => {
|
|
this.conns++;
|
|
const opened = Date.now();
|
|
const upstream = netConnect(this.targetPort, '127.0.0.1');
|
|
const pipeDelayed = (from, to) => {
|
|
from.on('data', (chunk) => {
|
|
this.bytes += chunk.length;
|
|
const wait = Date.now() - opened < HANDSHAKE_MS ? 0 : DELAY_MS;
|
|
setTimeout(() => { if (!to.destroyed) to.write(chunk); }, wait);
|
|
});
|
|
from.on('end', () => setTimeout(() => { if (!to.destroyed) to.end(); }, DELAY_MS));
|
|
from.on('error', () => { if (!to.destroyed) to.destroy(); });
|
|
from.on('close', () => setTimeout(() => { if (!to.destroyed) to.destroy(); }, DELAY_MS + 1000));
|
|
};
|
|
pipeDelayed(client, upstream);
|
|
pipeDelayed(upstream, client);
|
|
});
|
|
this.server.listen(this.listenPort, '127.0.0.1', () => resolve(this));
|
|
});
|
|
}
|
|
stop() { try { this.server?.close(); } catch { } }
|
|
}
|
|
|
|
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=${LOGLEVEL}`, '--yes'];
|
|
if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0');
|
|
const out = openSync(this.logFile, 'a');
|
|
const env = { ...process.env, ...(PIPELINE != null ? { IGNEUM_RELAY_PIPELINE: String(PIPELINE) } : {}) };
|
|
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out], env });
|
|
started.push(this.proc);
|
|
await sleep(1500);
|
|
if (this.proc.exitCode != null) throw new Error(`n${this.i} exited ${this.proc.exitCode}: ${this.grepLog(/ERROR|Error|error|refused|invalid/).slice(-3).join(' | ')}`);
|
|
for (let i = 0; i < 20; i++) {
|
|
try { this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`); await this.rpc.call('getBlockDagInfo'); break; } catch { this.rpc = null; await sleep(500); }
|
|
}
|
|
if (!this.rpc) throw new Error(`n${this.i}: the RPC did not answer within 10 s`);
|
|
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}${this.connect.length ? ` via proxy ${this.connect.join(',')}` : ''}${PIPELINE != null ? ` relay pipeline ${PIPELINE}` : ''}`);
|
|
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;
|
|
}
|
|
const proxies = [];
|
|
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 { } }
|
|
for (const x of proxies) x.stop();
|
|
}
|
|
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
|
|
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
|
|
|
|
// bits -> expected hashes per block: target = mantissa x 2^(8 (exponent - 3)); hashes = 2^256 / (target + 1)
|
|
export function expectedHashes(bits) {
|
|
const exp = BigInt(bits >>> 24), mant = BigInt(bits & 0xffffff);
|
|
const target = exp <= 3n ? mant >> (8n * (3n - exp)) : mant << (8n * (exp - 3n));
|
|
const ratio = (1n << 256n) / (target + 1n);
|
|
return Number(ratio);
|
|
}
|
|
const minerHash = (i) => { // the newest `hash=<x> MH/s` status line of miner i, in hashes per second
|
|
let v = null;
|
|
try { for (const l of readFileSync(`${TMP}/cpu${i}.log`, 'utf8').split('\n')) { const m = /hash=([\d.]+) MH\/s/.exec(l); if (m) v = +m[1] * 1e6; } } catch { }
|
|
return v;
|
|
};
|
|
|
|
const n0 = await new Node(0).start();
|
|
const p1 = await new DelayProxy(BASE + 101, n0.p2pPort).start(); proxies.push(p1);
|
|
const p2 = await new DelayProxy(BASE + 102, n0.p2pPort).start(); proxies.push(p2);
|
|
const n1 = await new Node(1, [`127.0.0.1:${p1.listenPort}`]).start();
|
|
const n2 = await new Node(2, [`127.0.0.1:${p2.listenPort}`]).start();
|
|
const nodes = [n0, n1, n2];
|
|
await sleep(3000);
|
|
for (const n of nodes) { const d = await n.rpc.call('getBlockDagInfo'); log(`n${n.i} network ${d.network ?? d.networkName} blocks ${d.blockCount} difficulty ${d.difficulty} (bits ${d.difficulty * 2} hashes per block)`); }
|
|
const peers = await Promise.all(nodes.map(async n => { try { const r = await n.rpc.call('getConnectedPeerInfo'); const arr = Object.values(r).find(Array.isArray); return arr ? arr.length : '?'; } catch { return '?'; } }));
|
|
log(`peers per node ${peers.join(' ')} (hub 2, spokes 1)`);
|
|
nodes.forEach((n, i) => miner(CPU_MINER, ['mine', n.grpc, '1', String(SECS + 30), `cpu${i}`, '--engine', 'igneum-pow', '--payout-label', `cpu${i}`, '--status-secs', '30', '--no-vote'], `cpu${i}`, { IGNEUM_POW_DAY_MS: String(DAY_MS) }));
|
|
void devAddress('fast-time-dv3');
|
|
|
|
const t0 = Date.now();
|
|
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
|
|
const samples = [];
|
|
let lastReport = 0, measureStart = null;
|
|
while (Date.now() - t0 < SECS * 1000) {
|
|
await sleep(5000);
|
|
const dag = await Promise.all(nodes.map(async n => { try { return await n.rpc.call('getBlockDagInfo'); } catch (e) { return null; } }));
|
|
let bits = null;
|
|
try { const b = await n0.rpc.call('getBlock', { hash: dag[0].sink, includeTransactions: false }); bits = +b.block.header.bits; } catch { }
|
|
const hashes = [0, 1, 2].map(minerHash);
|
|
const hashSum = hashes.every(h => h != null) ? hashes.reduce((a, b) => a + b, 0) : null;
|
|
const expected = bits == null ? null : expectedHashes(bits);
|
|
const implied = hashSum == null ? null : hashSum * T_MS / 1000;
|
|
const s = { t: +since(), blocks: dag.map(d => d?.blockCount ?? null), daa: dag[0]?.virtualDaaScore ?? null, difficulty: dag[0]?.difficulty ?? null, bits, expected_hashes: expected, miner_hash: hashSum, hash_implied: implied, ratio: expected != null && implied ? expected / implied : null };
|
|
samples.push(s);
|
|
if (measureStart == null && s.t >= MEASURE_FROM && s.blocks[0] != null) measureStart = s;
|
|
if (Date.now() - lastReport > 30000) {
|
|
lastReport = Date.now();
|
|
const rate = samples.length > 6 ? ((s.blocks[0] - samples.at(-7).blocks[0]) / (s.t - samples.at(-7).t)).toFixed(2) : '?';
|
|
log(`t=${since()} s blocks ${s.blocks.join('/')} daa ${s.daa} rate(30s) ${rate}/s bits 0x${(bits ?? 0).toString(16)} expected ${expected?.toExponential(3)} hashes/block, miners ${hashSum == null ? '?' : (hashSum / 1e3).toFixed(1) + ' kH/s'} implied ${implied?.toExponential(3)} ratio ${s.ratio?.toFixed(3)}`);
|
|
}
|
|
}
|
|
await sleep(2000);
|
|
|
|
// ---- the reading ----
|
|
const end = samples.at(-1);
|
|
const measured = samples.filter(s => s.t >= MEASURE_FROM && s.ratio != null);
|
|
const dagRate = measureStart && end.blocks[0] != null ? (end.blocks[0] - measureStart.blocks[0]) / (end.t - measureStart.t) : null;
|
|
const rateOff = dagRate == null ? null : dagRate / BPS - 1;
|
|
const ratioMean = measured.length ? measured.reduce((a, s) => a + s.ratio, 0) / measured.length : null;
|
|
const diffOff = ratioMean == null ? null : ratioMean - 1;
|
|
// the red share and the chain step spacing from the hub's blocks since the measured window began
|
|
let reds = 0, chainBlocks = 0, merged = 0, spacings = [];
|
|
try {
|
|
const info = await n0.rpc.call('getBlockDagInfo');
|
|
let low = info.pruningPointHash; const seen = new Set(); let prevChainClock = null;
|
|
for (let round = 0; round < 400; round++) {
|
|
const r = await n0.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); added++;
|
|
if (!b.verboseData?.isChainBlock) continue;
|
|
const tMs = +b.header.timestamp;
|
|
if (tMs < t0 + MEASURE_FROM * 1000) continue;
|
|
chainBlocks++; reds += (b.verboseData.mergeSetRedsHashes || []).length; merged += (b.verboseData.mergeSetBluesHashes || []).length + (b.verboseData.mergeSetRedsHashes || []).length;
|
|
if (prevChainClock != null) spacings.push(tMs - prevChainClock); prevChainClock = tMs;
|
|
}
|
|
if (!blocks.length || added === 0) break;
|
|
low = (r.blockHashes || []).at(-1) || blocks.at(-1).verboseData?.hash; if (!low) break;
|
|
}
|
|
} catch (e) { log(`getBlocks: ${e.message}`); }
|
|
spacings.sort((a, b) => a - b);
|
|
const median = spacings.length ? spacings[Math.floor(spacings.length / 2)] : null;
|
|
// the relay reading (ledger N3): blocks the hub took via relay against via submit, from its own Accepted lines
|
|
let viaRelay = 0, viaSubmit = 0;
|
|
for (const l of n0.grepLog(/Accepted \d+ blocks/)) { const m = /(\d+) via relay and (\d+) via submit/.exec(l); if (m) { viaRelay += +m[1]; viaSubmit += +m[2]; } }
|
|
const checks = {
|
|
measured_window_present: measured.length >= 10,
|
|
dag_rate_within_10pct: rateOff != null && Math.abs(rateOff) <= 0.10,
|
|
difficulty_within_10pct_of_hash: diffOff != null && Math.abs(diffOff) <= 0.10,
|
|
three_nodes_within_5pct_blocks: end.blocks.every(b => b != null) && Math.max(...end.blocks) - Math.min(...end.blocks) <= 0.05 * Math.max(...end.blocks),
|
|
};
|
|
const good = checks.measured_window_present && checks.dag_rate_within_10pct && checks.difficulty_within_10pct_of_hash;
|
|
const pass = EXPECT === 'pass' ? good : !good;
|
|
const summary = {
|
|
pass, expect: EXPECT, case: CASE, relay_pipeline: PIPELINE, bps: BPS, target_time_ms: T_MS, propagation_ms: PROPAGATION_MS, delay_ms_per_leg: DELAY_MS, handshake_ms: HANDSHAKE_MS, rule: RULE, genesis_bits: GENESIS_BITS, secs: SECS, measure_from: MEASURE_FROM,
|
|
checks, dag_rate_measured: dagRate, dag_rate_off: rateOff, difficulty_over_hash_implied_mean: ratioMean, difficulty_off: diffOff,
|
|
measured_samples: measured.length, end_blocks: end.blocks, end_daa: end.daa, end_bits: end.bits, end_expected_hashes: end.expected_hashes, end_miner_hash: end.miner_hash,
|
|
chain_blocks_measured: chainBlocks, merged_blocks_measured: merged, red_blocks_measured: reds, red_share: merged ? reds / merged : null,
|
|
chain_step_spacing_ms: { median, p90: spacings.length ? spacings[Math.floor(spacings.length * 0.9)] : null, max: spacings.at(-1) ?? null },
|
|
hub_accepted: { via_relay: viaRelay, via_submit: viaSubmit, relay_share: viaRelay + viaSubmit ? viaRelay / (viaRelay + viaSubmit) : null },
|
|
proxy: { connections: p1.conns + p2.conns, bytes: p1.bytes + p2.bytes },
|
|
node: IGNEUMD, miner: CPU_MINER, samples,
|
|
};
|
|
mkdirSync(OUT.replace(/\/[^/]+$/, ''), { recursive: true });
|
|
writeFileSync(OUT, JSON.stringify(summary, null, 2));
|
|
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
|
|
const fails = Object.entries(checks).filter(([, v]) => !v).map(([k]) => k);
|
|
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'} (expect ${EXPECT}, ${CASE}): DAG rate ${dagRate?.toFixed(3)}/s against ${BPS} (${rateOff == null ? '?' : (100 * rateOff).toFixed(1) + '%'}), difficulty / hash-implied ${ratioMean?.toFixed(3)} (${diffOff == null ? '?' : (100 * diffOff).toFixed(1) + '%'}) over ${measured.length} samples; red share ${summary.red_share == null ? '?' : (100 * summary.red_share).toFixed(1) + '%'} of ${merged} merged over ${chainBlocks} chain blocks, chain step median ${median} ms p90 ${summary.chain_step_spacing_ms.p90} ms; hub took ${viaRelay} via relay and ${viaSubmit} via submit; blocks ${end.blocks.join('/')}; proxy ${summary.proxy.connections} conns ${summary.proxy.bytes} bytes${fails.length ? `; FAILED CHECK ${fails.join(', ')}` : ''}`);
|
|
log(`summary: ${OUT}`);
|
|
await stopAll();
|
|
process.exit(pass ? 0 : 1);
|