From 18:23Z both Windows workers (CUDA on PC 1 and PC 2, OpenCL on PC 1 after the 18:34Z node restart) refused every pack for epoch 34 with "the epoch seed bytes do not give the pack's IGNEUM_SEEDW_INIT", and the miner and the app restarted them every 5 to 60 s until 18:44Z and beyond. The packs were correct. The generator retries a rejected candidate with seed || k_le32 (attempt_words); epoch 34's attempt 0 was rejected (246 of 16384 final register values saturated, limit 163) and attempt 1 accepted, so the pack carried attempt 1's words while pf_load (proto-cuda/nvrtc/packfile.h) derived the expected words from the bare seed. Both workers also matched jobs to pairs by those bare-seed words, so even a loaded pack of a retried program would have answered "epoch seed mismatch" on every job. The rule, in one place per language: - packfile.h: pf_program_words(bytes, attempt); pf_load reads IGNEUM_PROGRAM_ATTEMPT and checks the attempt's words; the refusal says "program pack and its seeds disagree: IGNEUM_SEEDW_INIT is not attempt N of the epoch seed ..." in plain words. - worker.cpp and proto-opencl/host.c: a job belongs to a pair when the seed hex the node sent is the pair's (pairIs); the compiled-in placeholder pack keeps the word comparison. - igneum-pow/src/packcheck.rs: verify_pack_texts / verify_pack_dir, the same rule in Rust; the miner checks every pack it writes with it before a worker sees it (vendor/igneum-node pack-loop branch). Tests pin the attempt vectors of epoch 34 on both sides (one vector, two implementations), that epoch 34 is attempt 1 and epoch 33 attempt 0, a known-good pack of a later attempt, a known-mismatched (out of date) pack, and self-contradicting packs. - proto-cuda/nvrtc/emu/packfile-test.c (+ .sh, in CI): pf_load on a known-good attempt-1 pack, the checked-in attempt-0 pack, and the known-mismatched bare-words pack. The app (app/igneum-app): - watchdog.rs: PACK_OUT_OF_DATE_CODE 44, PackRebuilds (at most 3 pack exports per epoch, then the card shows the reason), pack_refusal (the worker's "error 0 pack" line and the miner's "PACK OUT OF DATE" line), pack_epoch_of; tests on the incident lines, known-good and known-mismatched. - engine.rs: exit 44 exports the pack again before the restart instead of a blind restart, the strip says "program pack out of date, rebuilding", the card and the log name the condition; at the cap the card is marked failed with the reason and tries again in 10 minutes. The relay (relay/lib/parse.mjs): PACK_MISMATCH; the card reads "pack mismatch, rebuilding (N refusals in the tail, M restarts)" in `node tools/console.mjs machines` instead of a bare restart count; tests on the PC 2 tail of 18:27Z and a healthy tail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
115 lines
9.4 KiB
JavaScript
115 lines
9.4 KiB
JavaScript
// node --test relay/test/parse.test.mjs relay/test/auth.test.mjs (no dependencies; CI runs both in the site job)
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { parseLabel, parseMinerTail, markStale, STALE_S, PACK_MISMATCH } from '../lib/parse.mjs';
|
|
|
|
test('labels: every vendor the app names, the app log, the node log, the legacy launchers', () => {
|
|
assert.deepEqual(parseLabel('miner-nvidia-ae432dc7-1'), { id: 'ae432dc7', platform: 'win', stream: 'miner', vendor: 'nvidia', card: 1 });
|
|
assert.deepEqual(parseLabel('miner-other-37ba0461-1'), { id: '37ba0461', platform: 'win', stream: 'miner', vendor: 'other', card: 1 });
|
|
assert.deepEqual(parseLabel('miner-intel-37ba0461-2'), { id: '37ba0461', platform: 'win', stream: 'miner', vendor: 'intel', card: 2 });
|
|
assert.deepEqual(parseLabel('miner-mac-d937c69d-1'), { id: 'd937c69d', platform: 'mac', stream: 'miner', vendor: 'mac', card: 1 });
|
|
assert.deepEqual(parseLabel('nodelog-win-1ccfe586'), { id: '1ccfe586', platform: 'win', stream: 'node' });
|
|
assert.deepEqual(parseLabel('win-1ccfe586'), { id: '1ccfe586', platform: 'win', stream: 'app' });
|
|
assert.equal(parseLabel('nodelog-DESKTOP-KMCV30N').platform, 'legacy');
|
|
assert.equal(parseLabel('job-run'), null);
|
|
});
|
|
|
|
test('miner tail: the last STATUS line gives the numbers, ACCEPTED lines are counted without one', () => {
|
|
const tail = [
|
|
"1791140549.352 STATUS 'other-37ba0461-1' [worker]: 30s jobs=15 accepted=1 rejected=0 mismatched=0 extra=0 rate=0.03 blocks/s hash=1.04 MH/s wall (1.57 MH/s inside jobs) now=1.04 MH/s wall",
|
|
'1791140553.360 ACCEPTED block nonce=0xa27b8e5b000a82f6 (gpu worker, cpu re-check ok, identity other-37ba0461-1)',
|
|
"1791140579.479 STATUS 'other-37ba0461-1' [worker]: 61s jobs=37 accepted=2 rejected=0 mismatched=0 extra=0 rate=0.02 blocks/s hash=1.28 MH/s wall (1.55 MH/s inside jobs) now=1.53 MH/s wall",
|
|
].join('\n');
|
|
const c = parseMinerTail(tail);
|
|
assert.equal(c.accepted, 2); assert.equal(c.hash_now, 1.53); assert.equal(c.hash_avg, 1.28); assert.equal(c.uptime_s, 61);
|
|
assert.equal(c.status_at, new Date(1791140579479).toISOString());
|
|
assert.equal(c.faults, 0);
|
|
const noStatus = parseMinerTail('1 ACCEPTED block x\n2 ACCEPTED block y\n3 WORKER MISMATCH z');
|
|
assert.equal(noStatus.accepted, 2); assert.equal(noStatus.status_at, null); assert.equal(noStatus.faults, 1);
|
|
});
|
|
|
|
test('stale: a card is "now" within STALE_S of its STATUS line and stale after it or without one', () => {
|
|
const now = Date.parse('2026-10-04T19:03:00Z');
|
|
const fresh = { status_at: new Date(now - 58_000).toISOString(), hash_now: 18.9 };
|
|
const old = { status_at: new Date(now - 4 * 60_000).toISOString(), hash_now: 117.2 };
|
|
const none = { status_at: null, hash_now: 0 };
|
|
markStale([fresh, old, none], now);
|
|
assert.equal(fresh.stale, false); assert.equal(fresh.status_age_s, 58);
|
|
assert.equal(old.stale, true); assert.equal(old.status_age_s, 240);
|
|
assert.equal(none.stale, true); assert.equal(none.status_age_s, null);
|
|
assert.ok(STALE_S >= 160, 'one missed 60 s upload plus a 30 s STATUS age plus the 10 s cache is not stale');
|
|
const oneMissed = { status_at: new Date(now - 150_000).toISOString(), hash_now: 18 };
|
|
markStale([oneMissed], now); assert.equal(oneMissed.stale, false);
|
|
});
|
|
|
|
test('app tail: a clean quit after the last status line is "stopped (quit)", an update is "stopped (update)", a running app is not stopped', async () => {
|
|
const { parseAppTail } = await import('../lib/parse.mjs');
|
|
const running = ['1791126700 status: accepted 10 blocks (5 this run), 18.00 MH/s, mining | node 100 blocks, 4 peers, synced | up 00:10:00'].join('\n');
|
|
assert.equal(parseAppTail(running).stopped, undefined);
|
|
const quit = [
|
|
'1791126700 status: accepted 10 blocks (5 this run), 18.00 MH/s, mining | node 100 blocks, 4 peers, synced | up 00:10:00',
|
|
'1791126735 quit: stopping the miners, then the node', '1791126735 stopping the miners (quit)', '1791126736 node stopped (exit Some(0))',
|
|
'1791126736 SUMMARY after 00:26:53: node started 1 time(s), restarts 0, 1165 accepted blocks this run (1270 lifetime); data stays in x', '1791126737 stopped',
|
|
].join('\n');
|
|
const q = parseAppTail(quit);
|
|
assert.deepEqual([q.stopped.reason, q.stopped.at], ['quit', new Date(1791126737000).toISOString()]);
|
|
const update = [
|
|
'1791125100 status: accepted 1 blocks (1 this run), 1.00 MH/s, mining | node 1 blocks, 1 peers, synced | up 00:01:00',
|
|
'1791125117 [info] installing Igneum Miner 0.3.2: the miners stop, then the node, then the app opens again',
|
|
'1791125118 quit: stopping the miners, then the node', '1791125121 stopped',
|
|
].join('\n');
|
|
assert.equal(parseAppTail(update).stopped.reason, 'update');
|
|
// a quit line from an earlier run in the same tail, with a status line after it: still running
|
|
const resumed = quit + '\n1791126800 status: accepted 11 blocks (1 this run), 18.00 MH/s, mining | node 120 blocks, 4 peers, synced | up 00:01:00';
|
|
assert.equal(parseAppTail(resumed).stopped, undefined);
|
|
});
|
|
|
|
test('app tail: the newest update line decides the OTA state: staged beats an older check, a finished run says current, a newer OTA line wins', async () => {
|
|
const { parseAppTail } = await import('../lib/parse.mjs');
|
|
const st = n => `${n} status: accepted 1 blocks (1 this run), 1.00 MH/s, mining | node 1 blocks, 1 peers, synced | up 00:01:00`;
|
|
const mac = ['1791141355 update check: 0.3.3 is current (manifest 0.3.3)', st(1791145000),
|
|
'1791145529 [info] Igneum Miner 0.3.4 is available: downloading (39 MB). Finality rule v3',
|
|
'1791145533 update: /Users/x/Library/Application Support/Igneum/app/updates/Igneum-Miner-0.3.4.dmg downloaded and verified',
|
|
'1791145562 update: staged bundle digest 2b640b8c', '1791145562 [ok] Igneum Miner 0.3.4 is ready; it installs at the next safe moment', st(1791146000)].join('\n');
|
|
const m = parseAppTail(mac);
|
|
assert.deepEqual([m.ota.state, m.ota.version, m.ota.at], ['staged', '0.3.4', new Date(1791145562000).toISOString()]);
|
|
const after = ['1791146000 IGNEUM-APP version=0.3.4 machine=1ccfe586 platform=windows node=igneumd/2.1.0', '1791146426 update check: 0.3.4 is current (manifest 0.3.4)', st(1791146430)].join('\n');
|
|
const a = parseAppTail(after);
|
|
assert.deepEqual([a.ota.state, a.ota.version], ['current', '0.3.4']);
|
|
assert.notEqual(parseAppTail(after + "\n1791146500 OTA: 0.3.5 downloaded, waiting for this machine's minute").ota.state, 'current');
|
|
assert.equal(parseAppTail('1791146400 OTA: 0.3.4 installed\n' + after).ota.state, 'current');
|
|
assert.equal(parseAppTail(mac + '\n1791146600 update: 0.3.4 is marked failed; it will not be applied again by itself').ota.state, 'failed');
|
|
assert.equal(parseAppTail(mac + '\n1791146600 update to 0.3.4 complete (from 0.3.3); keeping the previous version for a rollback').ota.state, 'updated');
|
|
assert.equal(parseAppTail(st(1)).ota, undefined);
|
|
const inst = parseAppTail(mac + '\n1791146700 [info] installing Igneum Miner 0.3.4: the miners stop, then the node, then the app opens again');
|
|
assert.deepEqual([inst.ota.state, inst.ota.version], ['installing', '0.3.4']);
|
|
});
|
|
|
|
test('pack mismatch: the refused-pack tail of 5 October 2026 reads "pack mismatch, rebuilding", a healthy tail does not', () => {
|
|
// PC 2, nvidia-1ccfe586-1, 18:27 to 18:28 UTC: the worker refused packs\devnet at every start and the miner restarted it
|
|
const refused = [
|
|
"1791224818.418 epoch seed 009858237e118f69abc8d096e9b1af21c24539eaecdfd1b896588825660a69ec day 20731 (daa 122513): CPU program and cache ready in 179 ms",
|
|
"! 1791224818.419 worker error: error 0 pack packs\\devnet: the epoch seed bytes do not give the pack's IGNEUM_SEEDW_INIT (wrong seeds.txt for this pack?)",
|
|
'! 1791224818.933 worker exited (code Some(1)); restarting it in 21 s (restart 1)',
|
|
"1791224840.037 STATUS 'nvidia-1ccfe586-1' [worker]: 22s jobs=0 accepted=0 rejected=0 fee=0 mismatched=0 extra=0 rate=0.00 blocks/s hash=0.00 MH/s wall (0.00 MH/s inside jobs) now=0.00 MH/s wall (0.00 MH/s inside jobs, 0 jobs, seed walk 0 calls) template_age=0.45s synced=true idle=100.0% (last 22s: 100.0%) queued=2 restarts=1 faults=0 identities=8 accepted_by_identity=0/0/0/0/0/0/0/0",
|
|
"! 1791224840.037 worker error: error 0 pack packs\\devnet: the epoch seed bytes do not give the pack's IGNEUM_SEEDW_INIT (wrong seeds.txt for this pack?)",
|
|
'! 1791224840.548 worker exited (code Some(1)); restarting it in 41 s (restart 2)',
|
|
].join('\n');
|
|
const c = parseMinerTail(refused);
|
|
assert.equal(c.pack_mismatch, true);
|
|
assert.equal(c.fault, 'pack mismatch, rebuilding (2 refusals in the tail, 1 restarts)');
|
|
assert.equal(c.restarts, 1);
|
|
// the miner's own line (0.3.11 miners) counts too
|
|
const own = parseMinerTail('1791224840.100 PACK OUT OF DATE packs\\devnet: the worker refused its program pack; rebuilding the program pack before the restart');
|
|
assert.equal(own.pack_mismatch, true);
|
|
assert.ok(own.fault.startsWith('pack mismatch, rebuilding'));
|
|
// known-good: a mining tail with a job error that is not a refusal keeps the plain fault text
|
|
const healthy = [
|
|
"1791140579.479 STATUS 'other-37ba0461-1' [worker]: 61s jobs=37 accepted=2 rejected=0 mismatched=0 extra=0 rate=0.02 blocks/s hash=1.28 MH/s wall (1.55 MH/s inside jobs) now=1.53 MH/s wall",
|
|
'! 1791140580.000 worker error: error 17 epoch seed mismatch: this worker holds epoch bed7ab62cbece66c',
|
|
].join('\n');
|
|
const h = parseMinerTail(healthy);
|
|
assert.equal(h.pack_mismatch, false);
|
|
assert.ok(h.fault.includes('worker error: error 17'));
|
|
assert.equal(PACK_MISMATCH.test('1 ACCEPTED block x'), false);
|
|
});
|