The statement and the pinned guests are unchanged; every fixture proof verifies as before. The defaults stay (batch-log2 22, SP1 defaults): the one knob that moves a mining card's prover costs a fifth of the hash rate; the plan carries the trade for the project lead and the batch fold for the next pin. Measured: docs/bench-log.md "aggregation cost on the RTX 5090"; the plan line: docs/plans/proving-v1.md "Aggregation cost (5 October, night)". Also: make-package's gate skips the exporter's .node-plan.json side files and takes the run lock for its execute step; the state-reply class (/api/state answering {} once paid_wei passes u64::MAX) found on the way and fixed on the app branch at 42f36b3.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit ea38ece9eaa5949dd657cbfea5308c4948177ff8)
35 lines
2.5 KiB
JavaScript
35 lines
2.5 KiB
JavaScript
#!/usr/bin/env node
|
|
// The hash rate of one miner over a UTC window, from its STATUS lines in the log intake (the app uploads the miner's
|
|
// log every minute; each STATUS line starts with a unix timestamp and carries `now=<MH/s> MH/s wall`, the last
|
|
// interval's rate). For the aggregation-cost phases that keep the app's own miner running (docs/bench-log.md,
|
|
// 5 October 2026 night), where the job cannot read the app's state from PowerShell 5.1.
|
|
// node tools/proving-v1/miner-rate.mjs <label> <start ISO> <end ISO> e.g. miner-nvidia-1ccfe586-1 2026-10-05T21:00:00Z 2026-10-05T21:05:00Z
|
|
import { readFileSync } from 'node:fs';
|
|
import { homedir } from 'node:os';
|
|
const [label, start, end] = process.argv.slice(2);
|
|
if (!label || !start || !end) { console.error('usage: miner-rate.mjs <label> <start ISO> <end ISO>'); process.exit(2); }
|
|
const m = /^DATABASE_URL=(.*)$/m.exec(readFileSync(`${homedir()}/.config/igneum/env`, 'utf8'));
|
|
const url = m[1].trim().replace(/^['"]|['"]$/g, '');
|
|
const host = new URL(url).hostname.replace('-pooler', '');
|
|
const sql = async (query, params = []) => {
|
|
const r = await fetch(`https://${host}/sql`, { method: 'POST', headers: { 'Neon-Connection-String': url, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, params }) });
|
|
const j = await r.json();
|
|
if (!r.ok) throw new Error(j.message || JSON.stringify(j));
|
|
return j.rows;
|
|
};
|
|
const t0 = Date.parse(start) / 1000, t1 = Date.parse(end) / 1000;
|
|
// every upload received up to 15 min after the window (the log is re-uploaded whole, so the newest covering upload holds every line)
|
|
const rows = await sql(`SELECT received_at, lines FROM miner_logs WHERE label = $1 AND received_at > to_timestamp($2) AND received_at < to_timestamp($3) ORDER BY received_at DESC LIMIT 20`, [label, t0, t1 + 900]);
|
|
const seen = new Map();
|
|
for (const r of rows) {
|
|
for (const l of r.lines.split('\n')) {
|
|
const mm = /^(\d+(?:\.\d+)?) STATUS .* now=([\d.]+) MH\/s wall/.exec(l);
|
|
if (!mm) continue;
|
|
const t = Number(mm[1]);
|
|
if (t >= t0 && t <= t1) seen.set(mm[1], Number(mm[2]));
|
|
}
|
|
}
|
|
const h = [...seen.values()].sort((a, b) => a - b);
|
|
if (!h.length) { console.log(`${label} ${start}..${end}: no STATUS lines in the window (${rows.length} uploads read)`); process.exit(1); }
|
|
const mean = h.reduce((a, b) => a + b, 0) / h.length;
|
|
console.log(`${label} ${start}..${end}: n=${h.length} mean=${mean.toFixed(2)} p50=${h[Math.floor((h.length - 1) / 2)].toFixed(2)} min=${h[0].toFixed(2)} max=${h[h.length - 1].toFixed(2)} MH/s wall (now= of the STATUS lines, ${rows.length} uploads read)`);
|