'; }).join('');
+ $('upd-more').hidden = !m.more; if (!m.more) updMore = false;
+ $('upd-more').textContent = updMore ? 'Less' : 'What changed'; $('upd-more').setAttribute('aria-expanded', updMore ? 'true' : 'false');
+ $('upd-all').hidden = !updMore;
+ $('upd-mark').className = 'upd-mark ' + m.ring;
+ var acts = asked ? [{ act: 'install', label: m.stage === 'ready' || m.stage === 'deferred' ? 'Installing' : 'Installs when ready', primary: true, disabled: true }, { act: 'later', label: 'Later' }] : m.actions;
+ $('upd-actions').innerHTML = acts.map(function (a) { return ''; }).join('');
+ $('upd').dataset.dismiss = m.dismissable ? '1' : '';
+ }
+ // the ring and the percent move every poll; the words above do not
+ if (m.ring === 'progress') { $('upd-arc').style.strokeDashoffset = (276.5 * (1 - m.pct / 100)).toFixed(1); $('upd-pct').textContent = m.pct + '%'; $('upd-pct').hidden = m.stage !== 'downloading'; }
+ else { $('upd-arc').style.strokeDashoffset = ''; $('upd-pct').hidden = true; }
+ var meta = [];
+ if (m.stage === 'downloading' && m.size) meta.push(m.pct + '% of ' + m.size);
+ else if (m.size && (m.stage === 'available')) meta.push(m.size + ' download');
+ else if (m.size && (m.stage === 'ready' || m.stage === 'manual' || m.stage === 'deferred' || m.stage === 'staging')) meta.push(m.size + ', downloaded');
+ if (m.note) meta.push(m.note);
+ $('upd-meta').textContent = meta.join(' · ');
+ if (wrap.hidden) { wrap.hidden = false; $('upd-card').focus({ preventScroll: true }); }
+ }
+ function cardLater() {
+ if (!updMem.open || !$('upd').dataset.dismiss) return;
+ var m = UpdateCard.model(state && state.update, state || {});
+ updMem.later = m ? m.key : updMem.later; updMem.open = false; $('upd').hidden = true; updSig = '';
+ }
+ $('upd').addEventListener('click', function (e) {
+ if (e.target === $('upd')) { cardLater(); return; }
+ var b = e.target.closest('[data-act]'); if (!b || b.disabled) return;
+ var act = b.dataset.act, m = UpdateCard.model(state && state.update, state || {});
+ if (act === 'later') cardLater();
+ else if (act === 'install' || act === 'retry') { api('api/update/install', {}); updAsked = m ? m.key : ''; updSig = ''; toast(act === 'retry' ? 'Trying the update again' : m && m.stage === 'ready' ? 'Installing at once' : 'Installs as soon as it is downloaded'); if (state) renderUpdateCard(state); }
+ else if (act === 'open') api('api/update/open', {});
+ });
+ $('upd-more').addEventListener('click', function () { updMore = !updMore; $('upd-all').hidden = !updMore; this.textContent = updMore ? 'Less' : 'What changed'; this.setAttribute('aria-expanded', updMore ? 'true' : 'false'); });
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !$('upd').hidden) { e.preventDefault(); cardLater(); } });
+
// ---------- over-the-air updates (src/ota.rs): the settings note; the strip is Notices.updateNotice ----------
function settingsUpdateNote(u) {
var n = Notices.updateNotice(u, state || {}), parts = [];
@@ -864,16 +1024,19 @@ if (typeof document !== 'undefined') (function () {
return parts.join(' ');
}
function sampleUpdate(kind) {
- var u = { available: true, version: '0.3.1', notes: 'difficulty v2, OTA updates', status: 'ready', downloaded: true, ready: true, applying: false, progress: 1, size: 20588331, auto: true, wait: 'installs at the next safe moment', urgent: false, urgent_text: '', activation_height: 0, error: '', updated_from: '', rolled_back: '' };
+ // ?update=[&auto=0]: the card and the strip for each state, without an engine
+ var u = { available: true, version: '0.3.7', notes: 'The update card: one centred card, Install now and Later. Worker restarts no longer drop the hourly program. The log drawer wraps long lines and jumps to the last error. Windows Hello is written but untested; Touch ID confirms every send on the Mac.', status: 'ready', downloaded: true, ready: true, applying: false, progress: 1, size: 20588331, auto: params.get('auto') !== '0', wait: 'installs at the next safe moment', urgent: false, urgent_text: '', activation_height: 0, error: '', updated_from: '', rolled_back: '' };
+ if (!u.auto) u.wait = 'waiting for Install now (automatic updates are off)';
if (kind === 'available') { u.status = 'available'; u.downloaded = false; u.ready = false; u.progress = 0; }
- if (kind === 'downloading') { u.status = 'downloading'; u.downloaded = false; u.ready = false; u.progress = 0.43; }
+ if (kind === 'downloading') { u.status = 'downloading'; u.downloaded = false; u.ready = false; u.progress = Math.min(0.97, 0.43 + ((Date.now() / 1000) % 60) / 110); }
+ if (kind === 'staging') { u.status = 'staging'; u.ready = false; u.progress = 1; }
if (kind === 'waiting') { u.wait = 'hourly program boundary in 97 s; installing after it'; }
if (kind === 'applying') { u.status = 'applying'; u.applying = true; }
if (kind === 'deferred') { u.status = 'deferred'; u.wait = 'waits for the next time someone is at this PC (Windows asks for permission); mining continues'; }
- if (kind === 'urgent') { u.urgent = true; u.activation_height = 120000; u.urgent_text = 'Consensus upgrade at height 120000 (difficulty v2): the node is 1,240 blocks away. Installing 0.3.1 now.'; }
+ if (kind === 'urgent') { u.status = 'downloading'; u.ready = false; u.downloaded = false; u.progress = 0.27; u.urgent = true; u.activation_height = 120000; u.urgent_text = 'Consensus upgrade at height 120000 (difficulty v2): the node is 1,240 blocks away. Installing 0.3.7 now.'; }
if (kind === 'manual') { u.status = 'manual'; u.ready = false; u.wait = '/Applications is not writable; open the downloaded disk image and drag the app over the old one'; }
if (kind === 'error') { u.status = 'error'; u.error = 'sha256 mismatch: the file is not what the manifest signed'; u.downloaded = false; u.ready = false; }
- if (kind === 'updated') { u.status = 'current'; u.available = false; u.ready = false; u.downloaded = false; u.updated_from = '0.3.0'; u.version = '0.3.1'; }
+ if (kind === 'updated') { u.status = 'current'; u.available = false; u.ready = false; u.downloaded = false; u.updated_from = '0.3.6'; u.version = '0.3.7'; }
return u;
}
// ---------- remote jobs (src/jobrun.rs): the settings block; the strip is Notices.jobNotice ----------
@@ -906,11 +1069,13 @@ if (typeof document !== 'undefined') (function () {
}
function render(s) {
state = s; stateAt = performance.now();
- if (forcedUpdate) { s.update = sampleUpdate(forcedUpdate); if (forcedUpdate === 'updated') { s.version = '0.3.1'; s.uptime_s = 20; } }
+ if (forcedUpdate) { s.update = sampleUpdate(forcedUpdate); if (forcedUpdate === 'updated') { s.version = '0.3.7'; s.uptime_s = 20; } }
if (forcedJob) s.jobs = sampleJob(forcedJob, s.now || 0);
+ if (params.get('uptime')) s.uptime_s = parseInt(params.get('uptime'), 10) || 0; // the card's first-minute rule, for screenshots
renderPill(s);
renderClock(s);
renderNotices(s);
+ renderUpdateCard(s);
if (phase === 'cards') renderCards(s);
if (phase === 'dashboard') renderDashboard(s);
if (s.quitting && !$('btn-quit').disabled) { $('btn-quit').disabled = true; }
diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html
index e99c75909..ab1f16256 100644
--- a/app/igneum-app/ui/index.html
+++ b/app/igneum-app/ui/index.html
@@ -218,6 +218,26 @@
+
+
+
+
+
+
+
+
+
update
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/igneum-app/ui/update-card.test.mjs b/app/igneum-app/ui/update-card.test.mjs
new file mode 100644
index 000000000..881a9e0b8
--- /dev/null
+++ b/app/igneum-app/ui/update-card.test.mjs
@@ -0,0 +1,118 @@
+// node --test app/igneum-app/ui/update-card.test.mjs (no dependencies; CI runs it in the site job)
+// Loads the UpdateCard block at the top of app.js (plain browser JS, run with `module` defined and no `document`)
+// and checks what the card says for each update state, the release-note lines, and when it shows or waits.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+
+const src = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'app.js'), 'utf8');
+const mod = { exports: {} };
+new Function('module', src)(mod);
+const C = mod.exports.UpdateCard;
+const { model, decide, blocked, splitNotes, sentences, size } = C;
+
+const NOTES = 'Difficulty rule v2 from DAA score 33,000 (about 18:35 UTC on 4 October); every node must carry it before then. The log drawer wraps long lines. Touch ID on the Mac. Windows Hello is written but untested.';
+const upd = (over) => ({ available: true, version: '0.3.7', notes: NOTES, checked_at: 0, error: '', status: 'ready', downloaded: true, ready: true, applying: false, progress: 1, size: 20_588_331, auto: true, wait: '', urgent: false, urgent_text: '', activation_height: 0, unsupported: false, min_supported: '', channel: 'devnet', published_at: '', file: '', updated_from: '', rolled_back: '', ...over });
+const quiet = { jobActive: false, uptime_s: 5000, quitting: false, sheetOpen: false };
+
+test('release notes: sentences, three lines under 70 characters, the rest behind What changed', () => {
+ assert.deepEqual(sentences('one. two! three? four; five'), ['One.', 'Two!', 'Three?', 'Four', 'Five']);
+ assert.deepEqual(sentences('DAA score 33,000 is the switch. v2.1 ships.'), ['DAA score 33,000 is the switch.', 'V2.1 ships.']);
+ assert.deepEqual(sentences('line one\nline two\n\n'), ['Line one', 'Line two']);
+ assert.deepEqual(sentences(''), []);
+ const n = splitNotes(NOTES);
+ assert.equal(n.lines.length, 3);
+ for (const l of n.lines) assert.ok(l.length < C.LINE_MAX, `${l.length}: ${l}`);
+ assert.equal(n.lines[0], 'Difficulty rule v2 from DAA score 33,000 (about 18:35 UTC on 4…');
+ assert.equal(n.lines[1], 'Every node must carry it before then');
+ assert.equal(n.lines[2], 'The log drawer wraps long lines');
+ assert.equal(n.more, true);
+ assert.equal(n.all.length, 5);
+ const short = splitNotes('Faster sync. Fewer restarts.');
+ assert.deepEqual(short.lines, ['Faster sync', 'Fewer restarts']); assert.equal(short.more, false);
+ assert.deepEqual(splitNotes('').lines, []); assert.equal(splitNotes('').more, false);
+ assert.equal(size(20_588_331), '21 MB'); assert.equal(size(612_000), '0.6 MB'); assert.equal(size(0), '');
+});
+
+test('what the card says: available, downloading with the ring, ready, installing, failed, manual, waiting', () => {
+ const a = model(upd({ status: 'available', downloaded: false, ready: false, progress: 0 }), {});
+ assert.equal(a.name, 'Igneum Ember 0.3.7'); assert.equal(a.line, 'is available.'); assert.equal(a.stage, 'available');
+ assert.equal(a.key, 'update:0.3.7:pending'); assert.equal(a.ring, 'none'); assert.equal(a.size, '21 MB');
+ assert.deepEqual(a.actions.map((x) => x.label), ['Install now', 'Later']); assert.equal(a.dismissable, true);
+ const d = model(upd({ status: 'downloading', downloaded: false, ready: false, progress: 0.43 }), {});
+ assert.equal(d.line, 'is downloading.'); assert.equal(d.pct, 43); assert.equal(d.ring, 'progress'); assert.equal(d.key, a.key);
+ assert.equal(model(upd({ status: 'staging', ready: false }), {}).key, a.key);
+ const r = model(upd(), {});
+ assert.equal(r.line, 'is ready to install.'); assert.equal(r.note, 'It installs by itself at a quiet moment.'); assert.equal(r.key, 'update:0.3.7:ready'); assert.equal(r.ring, 'full');
+ assert.equal(model(upd({ auto: false, wait: 'waiting for Install now (automatic updates are off)' }), {}).note, '');
+ assert.equal(model(upd({ wait: 'this version failed to install before; it waits for Install now' }), {}).note, 'It failed to install before.');
+ const i = model(upd({ status: 'applying', applying: true }), {});
+ assert.equal(i.stage, 'installing'); assert.equal(i.line, 'Installing. The app restarts itself.'); assert.deepEqual(i.actions, []); assert.equal(i.dismissable, false); assert.equal(i.ring, 'busy');
+ assert.equal(model(upd({ wait: 'installing now' }), {}).stage, 'installing');
+ const f = model(upd({ status: 'error', error: 'sha256 mismatch: the file is not what the manifest signed\nsecond line', ready: false }), {});
+ assert.equal(f.stage, 'failed'); assert.equal(f.line, 'did not install.'); assert.equal(f.cause, 'sha256 mismatch: the file is not what the manifest signed');
+ assert.deepEqual(f.actions.map((x) => x.label), ['Try again', 'Later']); assert.equal(f.key, 'update:0.3.7:failed');
+ assert.equal(model(upd({ status: 'error', error: 'did not stay up', rolled_back: '0.3.7: did not stay up' }), {}).line, 'did not stay up and was rolled back.');
+ const m = model(upd({ status: 'manual', ready: false, wait: '/Applications is not writable' }), {});
+ assert.equal(m.line, 'is downloaded.'); assert.equal(m.actions[0].label, 'Open the download');
+ assert.equal(model(upd({ status: 'deferred' }), {}).line, 'is waiting for permission.');
+ // urgent: no Later, the engine's words as the note
+ const u = model(upd({ status: 'downloading', progress: 0.2, urgent: true, urgent_text: 'Consensus upgrade at height 120000. Installing now.' }), {});
+ assert.equal(u.dismissable, false); assert.deepEqual(u.actions.map((x) => x.label), ['Install now']); assert.equal(u.note, 'Consensus upgrade at height 120000. Installing now.');
+ // nothing to show
+ assert.equal(model(upd({ status: 'current', available: false }), {}), null);
+ assert.equal(model(upd({ status: 'checking' }), {}), null);
+ assert.equal(model(upd({ status: 'error', error: 'no update manifest configured in this build' }), {}), null);
+ assert.equal(model(upd({ status: 'error', version: '', error: 'manifest: connection refused' }), {}), null);
+ assert.equal(model(null, {}), null);
+});
+
+test('deferred: a running job, the first 60 s, the key sheet, quitting; installing is never hidden', () => {
+ assert.equal(blocked(quiet), '');
+ assert.equal(blocked({ ...quiet, jobActive: true }), 'a job is running');
+ assert.equal(blocked({ ...quiet, uptime_s: 59 }), 'the engine started under a minute ago');
+ assert.equal(blocked({ ...quiet, uptime_s: 60 }), '');
+ assert.equal(blocked({ ...quiet, sheetOpen: true }), 'the key sheet is open');
+ assert.equal(blocked({ ...quiet, quitting: true }), 'the app is quitting');
+ const a = model(upd({ status: 'available' }), {});
+ assert.deepEqual(decide(a, { ...quiet, jobActive: true }, {}), { show: false, why: 'deferred', reason: 'a job is running' });
+ assert.deepEqual(decide(a, { ...quiet, uptime_s: 10 }, {}), { show: false, why: 'deferred', reason: 'the engine started under a minute ago' });
+ assert.equal(decide(a, quiet, {}).show, true);
+ // the block lifts: the card comes (it was queued, not dismissed)
+ assert.equal(decide(a, { ...quiet, uptime_s: 61 }, { open: false, later: '', seen: '' }).show, true);
+ const i = model(upd({ status: 'applying', applying: true }), {});
+ assert.equal(decide(i, { ...quiet, jobActive: true }, { open: true }).show, true);
+});
+
+test('Later: hides this version at this stage; back for a newer version or a ready download with auto off', () => {
+ const pend = model(upd({ status: 'downloading', progress: 0.5, ready: false }), {});
+ const ready = model(upd(), {});
+ const readyOff = model(upd({ auto: false }), {});
+ assert.deepEqual(decide(pend, quiet, {}), { show: true, why: 'new' });
+ const later = { open: false, later: pend.key, seen: '0.3.7' };
+ assert.deepEqual(decide(pend, quiet, later), { show: false, why: 'later' });
+ // auto on: the ready download installs by itself, the strip says so, no second card
+ assert.deepEqual(decide(ready, quiet, later), { show: false, why: 'auto' });
+ // auto off: the ready download needs a click, so the card returns once
+ assert.deepEqual(decide(readyOff, quiet, later), { show: true, why: 'ready' });
+ assert.deepEqual(decide(readyOff, quiet, { open: false, later: readyOff.key, seen: '0.3.7' }), { show: false, why: 'later' });
+ // a newer version is a new key
+ const newer = model(upd({ status: 'available', version: '0.3.8' }), {});
+ assert.deepEqual(decide(newer, quiet, later), { show: true, why: 'new' });
+ // ready first seen with auto on (the download happened while the card could not open): shown once
+ assert.deepEqual(decide(ready, quiet, { open: false, later: '', seen: '' }), { show: true, why: 'ready' });
+ // an open card follows its update: downloading, ready, installing, failed
+ const open = { open: true, later: '', seen: '0.3.7' };
+ assert.equal(decide(pend, quiet, open).why, 'open'); assert.equal(decide(ready, quiet, open).why, 'open');
+ assert.equal(decide(model(upd({ status: 'applying', applying: true }), {}), quiet, open).show, true);
+ assert.equal(decide(model(upd({ status: 'error', error: 'x' }), {}), quiet, open).show, true);
+ // installing and failed without an open card: the strip has them
+ assert.deepEqual(decide(model(upd({ status: 'applying', applying: true }), {}), quiet, {}), { show: false, why: 'strip' });
+ assert.deepEqual(decide(model(upd({ status: 'error', error: 'x' }), {}), quiet, {}), { show: false, why: 'strip' });
+ // urgent cannot be dismissed
+ const urgent = model(upd({ status: 'downloading', progress: 0.2, urgent: true, urgent_text: 'Consensus upgrade.' }), {});
+ assert.deepEqual(decide(urgent, quiet, { later: urgent.key }), { show: true, why: 'urgent' });
+ assert.deepEqual(decide(null, quiet, {}), { show: false, why: 'none' });
+});
diff --git a/app/windows/version.h b/app/windows/version.h
index 4aed77d75..8a451d87f 100644
--- a/app/windows/version.h
+++ b/app/windows/version.h
@@ -3,6 +3,6 @@
// packaging/windows/Igneum-Miner.iss when the app version moves. Include guards, not #pragma once: rc.exe reads it too.
#ifndef IGNEUM_HOST_VERSION_H
#define IGNEUM_HOST_VERSION_H
-#define IGNEUM_HOST_VERSION_STR "0.3.7"
-#define IGNEUM_HOST_VERSION_RC 0,3,7,0
+#define IGNEUM_HOST_VERSION_STR "0.3.8"
+#define IGNEUM_HOST_VERSION_RC 0,3,8,0
#endif
diff --git a/docs/plans/release-0.3.8.md b/docs/plans/release-0.3.8.md
new file mode 100644
index 000000000..6e6cda0e6
--- /dev/null
+++ b/docs/plans/release-0.3.8.md
@@ -0,0 +1,199 @@
+# Igneum Miner 0.3.8: the cut, 5 October 2026
+
+Release engineer, from 13:1x UTC, the project lead away ("work your magic"). Worktree `/Users/joshm/Projects/igneum-wt-ship038`,
+branch `release-0.3.8` from master 8d6e6af. Every Mac build through `/Users/joshm/Projects/igneum/tools/lock/with-lock.sh build`
+at `nice -n 19` with `-j 4`. Times are UTC. The 0.3.6 and 0.3.7 cuts (`release-0.3.6.md`, sections 8 to 10) are the template.
+
+## 1. What 0.3.8 carries
+
+| Change | Where | Already on master |
+|---|---|---|
+| Pinned shard and aggregator guests: the host embeds `elf/`, verifies with the pinned key (about 2 s, was 114 to 138 s), `--mode id` | `proving/igneum-prove/elf/`, `host/src/pinned.rs`, b8b40a5, b588367, 8d6e6af | yes |
+| The WSL exporter path for Windows hosts (`PathBuf::join` wrote a backslash) | `app/igneum-app/src/prover.rs`, 6d51e53 | yes |
+| The WSL scripts' re-stamp prunes every target dir | 0861586 | yes |
+| The prover tools' source stamp, the empty-shard fixture tests | bb522db, 5bb7e86 | yes |
+| The update card (one centred card over the window when an update is ready) | `update-popup` f315112, merged as 97afe52 | merged here |
+
+Not merged: `biometrics` (the project lead has not tried it). The node does not change: fork 2b6d23ef, the 0.3.7 Windows exes
+(the Mac cross-build) and the Mac arm64 binaries, reused as files.
+
+## 2. The branch
+
+| Commit | What |
+|---|---|
+| 97afe52 | Merge `update-popup` (f315112). One conflict, `.github/workflows/ci.yml`: update-popup's test step taken (it runs `notices.test.mjs` and `update-card.test.mjs`; master's ran only the first) |
+| 7ce02b1 | `Igneum Miner 0.3.8: ...`, the six version files (`node tools/ship-app.mjs --check`: 0.3.8 in all 6) |
+| 560a7e0 | `site: rebuilt` (`node site/build.mjs` picks up the program id journey entry already on master) |
+
+A `vendor` symlink to the main checkout's `vendor/` (untracked) makes the relative node paths resolve, as in the 0.3.6 cut.
+
+## 3. Tests and checks, with the command
+
+| Tip | Command | Result |
+|---|---|---|
+| 97afe52 | `node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs` | 10 pass, 0 fail |
+| 7ce02b1 | `cargo test -p igneum-app` (under the build lock, this Mac: an app-only release, the PC build jobs are for the node) | ok: 75 (lib) + 26 (ota-sign) + 8 (prove-verify), 0 failed, 13:15 to 13:16Z |
+| 7ce02b1 | `tools/ci/identity-check.sh` | 0 hits over 206 files |
+| 7ce02b1 | `tools/ci/copied-sources-check.sh` | every copying build script re-stamps its sources |
+| 7ce02b1 | `tools/ci/pinned-guests-check.sh` | elf/ matches its manifest, no script builds a guest outside pin-guests.sh |
+| 7ce02b1 | `node tools/ci/check-workflow-shell.mjs` | 12 run blocks in 2 workflows, 25 .ps1, 0 findings |
+| 560a7e0 | `node site/build.mjs && node tools/ci/link-check.mjs` | 477 internal links across 10 pages, 0 broken |
+| 7ce02b1 | `node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs` | 15 pass |
+| 560a7e0 | `node tools/ship-app.mjs 0.3.8 ... --from ci --dry-run` | preflight ok: tree clean, 0.3.8 in all 6, fork 2b6d23ef, both exes, both Mac binaries, both folders, gh igneum-labs, live inputs 2b6d23ef built 13:25:35Z |
+
+## 4. The Mac binaries and the DMG (13:20 to 13:27Z, one chained script under the lock)
+
+The release target dirs were cloned by APFS (`cp -c`) from the main checkout, then `cargo build --release` for the app
+and `cargo build --release -p igneum-prove-export -p igneum-prove-host` in `proving/igneum-prove` (no Succinct
+toolchain: the host embeds `elf/`). `igneum-prove-host --mode id` on this tree:
+
+```
+shard program id 0x0dfade071ffc05a50be5f7e6640fb12638bac0ea63697ec252863f55658be16a (2825296 bytes, sha256 0x65901e2dd6cc5762)
+aggregator id 0x135e67e742fbbcc8303676765266f25a6520ab36299820280125541051fb6c62 (320264 bytes), pinned 2026-10-05T12:07:56Z, SP1 6.8.1 circuit v6.1.0
+```
+
+Both equal `elf/manifest.json`. The seven block fixtures ran `--mode native` with the new host (the package gate's
+native half; the package itself was made with `SKIP_GATE=1`, see 6). Then
+`NODE=/vendor/igneum-node-036/target-integration/release/igneumd MINER=.../igneum-miner packaging/mac/build-dmg.sh`
+(13:23:52 to 13:27:17Z): `prover: igneum-prove-host and igneum-prove-export from /proving/igneum-prove/target/release`,
+the same `--mode id` line printed by the script, fingerprints `477bb0ef` (intake key next) and `ed9c4d2e` (folder next).
+
+| Artefact | sha256 | Size |
+|---|---|---|
+| `packaging/mac/dist/Igneum-Miner-0.3.8.dmg` (build 202610051323; engine 0.3.8, node 2b6d23ef Mac arm64 igneumd 64138a17... and igneum-miner ef438be2... as in 0.3.6/0.3.7, the 0.3.8 prover host and export) | 53c12b6e84a1c1e638d3e28d1cebb8eaf1980620c6dff5958292893033b62871 | 41,247,419 |
+
+The DMG is 1.1 MB larger than 0.3.7's (40,156,739): the host now carries the two ELFs and keys.
+
+## 5. The Windows payload inputs (13:25Z)
+
+`IGNEUM_WIN_RELEASE=/vendor/igneum-node-036/target-integration/x86_64-pc-windows-gnu/release IGNEUM_NODE_SRC=/vendor/igneum-node-036 packaging/windows/push-inputs.sh`
+from the worktree: igneumd.exe 98a40af9... (50,773,504) and igneum-miner.exe eb77bf93... (10,778,624), the 0.3.7 Mac
+cross-build of 2b6d23ef, with this Mac's toolchain DLLs (libstdc++-6, libgcc_s_seh-1, libwinpthread-1). The gate
+`check-runtime-dlls.sh`: igneumd.exe <- libstdc++-6.dll, all 197 imported symbols exported (5,994 exports). No worker
+exes in the worktree (the 0.3.6 worktree had none either; the PC builds them). Signed inputs: zip
+b587ed19fac41fe4c2fdb075b0c17a430c6e15adcd253dca6a58655c757652b5 (26,669,995 bytes, 5 files), node commit 2b6d23ef
+(release-0.3.6), verified locally with the key compiled into the app, deployed, HTTP 200. `node-source.pin` unchanged.
+
+## 6. The prover package for PC 2
+
+`SKIP_GATE=1 proving/windows-wsl2/make-package.sh /igneum-prove-wsl2-038.zip` from the worktree:
+98c246146e89af7799efe10d08b0a6a87cd46e05e3c7dcf872559009a7273a38, 1,425,452 bytes; `package/proving/igneum-prove/elf/`
+carries the two ELFs, the two keys and `manifest.json`. The gate was skipped on instruction; its native half ran on
+the DMG's host (section 4), its execute half did not run here.
+
+## 7. The proving rollout order (proving/README.md), with times
+
+| Step | What | When |
+|---|---|---|
+| (1) provers off | Mac: already off (the app's prove setting, `enabled: false`, `status: off`). PC 2: run job `prove-off-pc2-038` (`POST /api/prove {"on":false}` from a PowerShell script, then the app state and the node's `igneum_getProvingStatus`), published 13:18:53Z. PC 2's 0.3.7 app refused that first file: `jobs: jobs file signature does not verify` (13:19:41Z); the local and live bytes were identical and both signers (main checkout, this tree) verified it, so the file was re-signed and redeployed (`publish-jobs.sh sign --deploy`, 13:23:04Z) and the same job ran at once. Unexplained; recorded in 11 | ran 13:23:57 to 13:24:08Z: `api/prove off: {"ok":true}`; PC 2's node pool `{entries 17, failed 1, pending 0, verified 210}`, paidShards 210, tip DAA 104125 |
+| (2) empty pool | The Mac app node's pool (127.0.0.1:26800, `igneum_getProvingStatus`): 17 pending at 13:18Z (PC 2's records, every one rejected by the Mac's old host: `NOT VERIFIED`), 13 at 13:25Z, 9 at 13:29Z, 7 at 13:31Z, 2 at 13:35Z (tip DAA 104780). The last record from PC 2 reached the Mac at 13:30:17Z (rejected). A 30-s watch for `entries 0, pending 0` reported nothing before it expired at 13:59Z (its curl treated the node restart as "not empty" and did not say so: the CLAUDE.md rule on watchers, again); at 14:00:03Z the restarted node showed `{entries 0, failed 0, pending 0}` at tip DAA 106245. The pool was therefore not OBSERVED empty before step (3); it is inferred: no record entered after 13:30Z and the Mac restarted at 13:53:34Z, 1,400 DAA later, past the 600-DAA record window | inferred by 13:53Z |
+| (3) new host on every node | Not by the update-now job: the Mac and PC 1 took 0.3.8 on their own hourly slots (PC 1 engine restart 13:42:01Z, 8 min 41 s after the manifest; Mac 13:53:34Z). PC 2 from `update-now-038` (14:00:39Z): see 9. PC 2's WSL tools from the package: `rebuild-prover-pc2-038` (see below) | |
+| (4) `--mode id` | Mac (the installed `/Applications/Igneum Miner.app/Contents/Resources/bin/igneum-prove-host --mode id`): shard `0x0dfade071ffc05a50be5f7e6640fb12638bac0ea63697ec252863f55658be16a`, aggregator `0x135e67e7...`. PC 2: see below | |
+| (5) provers on | PC 2: run job `prove-on-pc2-038` | see below |
+
+
+### 7a. PC 2's WSL tools from the package
+
+| Job | Published | Result |
+|---|---|---|
+| `fetch-prove-038` (fetch, `--dir prove --extract --extract-dir igneum-prove-wsl2-038 --fresh`) | 13:28:06Z | ran 13:28:50Z: the zip (1,425,452 bytes, sha256 ok) extracted into `%LOCALAPPDATA%\igneum\prove\igneum-prove-wsl2-038` |
+| `rebuild-prover-pc2-038` (run, PowerShell, after PC 2 was on 0.3.8) | 14:03:04Z | ran 14:03:34 to 14:03:45Z as root in Ubuntu-24.04: rsync of `package/` into `~/igneum-prove` (`--delete`, target kept), every source re-stamped (`-name target -prune`), `elf/` listed (the two ELFs, two keys, manifest.json; the package manifest's shard id `0x0dfade07...`), `cargo clean -p igneum-prove-core -p igneum-prove-export -p igneum-prove-host`, `cargo build --release -p igneum-prove-export -p igneum-prove-host --features igneum-prove-host/cuda` (Finished in 5.65 s: only the three crates rebuilt, every dependency cached from the 0.3.7 build), installed into `/opt/igneum` (host 72,990,472 bytes b4bdc0680420c533a267f4f1b67845d287edad666ed6ab421506db3085b03896, export 3,474,768 bytes f19481b041a8383fb42d4fc812ef4008d98a6040114af4049b96d2790b4f47dd; `ldd`: 0 libs missing). `/opt/igneum` holds only the two tools (nothing else there to keep). No `/home/*/igneum-prove/target/release` dir exists, so the app's probe falls through to `/opt/igneum` |
+| `--mode id` on PC 2 (`/opt/igneum/igneum-prove-host --mode id`, in the same job) | | `shard program id 0x0dfade071ffc05a50be5f7e6640fb12638bac0ea63697ec252863f55658be16a (2825296 bytes, sha256 0x65901e2dd6cc5762) aggregator id 0x135e67e742fbbcc8303676765266f25a6520ab36299820280125541051fb6c62`: equal to the Mac DMG's and the installed Mac app's. A 0.3.7 host has no `--mode id`, so the line is also the proof that the installed binary is the new build |
+| `prove-on-pc2-038` (run: `POST /api/prove {"on":true}`, then the app state) | 14:04:55Z | ran 14:05:27 to 14:05:47Z: `api/prove on: {"ok":true}`; the app's state 20 s later: `backend cuda`, `status proving`, `message "proving on the GPU"`, `current "block 68178 shard 0"`, keys 10, pool 0; PC 2's engine log: `prover: host /opt/igneum/igneum-prove-host (WSL2), CUDA` at 14:05:36Z, then `block 68178 shard 0 assigned ... export, cut, prove (CUDA), sign, submit` |
+
+## 8. The ship (13:27 to 13:34Z)
+
+`git push -u origin release-0.3.8` at 560a7e0 (13:27Z, the credential helper), `gh workflow run windows.yml --ref release-0.3.8` → run
+37316906713, started 13:27:14Z, green 13:32:30Z (parse checks; engine, window host, payload, installer, smoke run; the G13
+inputs step verified the signed inputs of section 5: built 13:25:35Z, node 2b6d23ef, repo commit 560a7e0).
+
+```
+node tools/ship-app.mjs 0.3.8 --node vendor/igneum-node-036 --branch release-0.3.8 --dl-both \
+ --activation-height 84100 --deadline-note "proving v0" --notes "Pinned proving programs: one program id on every machine, the verifier answers in about 2 s; the update card; the Windows exporter path fix; re-stamped WSL scripts; node 2b6d23ef unchanged" \
+ --from ci
+```
+
+| Step | Result |
+|---|---|
+| preflight | ok: tree 560a7e0 clean, 0.3.8 in all 6, fork 2b6d23ef, gh igneum-labs, live inputs 2b6d23ef |
+| ci | green after 4 min of polling |
+| fetch | Igneum-Miner-Setup-0.3.8.exe 18.9 MB, igneum-windows-app.zip 26.3 MB |
+| dmg | already (section 4) |
+| copy, mirror | the DMG into the OLD folder; 8 files into the NEXT folder (the DMG, the installer, the payload zip, the three payload-inputs files, the CI record, igneum-prove-wsl2.zip), 2 same |
+| manifest | `consensus.override` carried over from the OLD folder's 0.3.7 manifest; both manifests signed (key 8f186e37...), verified locally, same fields |
+| deploy | one deploy, 13:33Z |
+| verify | both folders: the three files HEAD 200 with the local sizes, GET sha256 ok, the manifest signature ok |
+| console | item #354 "Igneum Miner 0.3.8 shipped (mac+windows)"; exit 0 at 13:34:36Z, 337 s |
+
+| File | sha256 | Size |
+|---|---|---|
+| Igneum-Miner-0.3.8.dmg | 53c12b6e84a1c1e638d3e28d1cebb8eaf1980620c6dff5958292893033b62871 | 41,247,419 |
+| Igneum-Miner-Setup-0.3.8.exe | 86cffd144adb89b9d886be9a3b4c76b91ed78afc737b3a9fc19ce44a2d71afcc | 19,794,320 |
+| igneum-windows-app.zip | 8f08a3040ac7d2b0e95714f8e930452bb26656f62af08f3ba07376ab72884a22 | 27,591,281 |
+
+The live manifests, read 13:34:54Z, compared field by field with the 0.3.7 OLD-folder manifest saved before the cut:
+
+| Field | 0.3.7 (before) | 0.3.8 OLD folder | 0.3.8 NEXT folder |
+|---|---|---|---|
+| version, channel, min_supported_version | 0.3.7, devnet, 0.3.0 | 0.3.8, devnet, 0.3.0 | same |
+| published_at | 2026-10-05T10:16:29Z | 2026-10-05T13:33:20Z | 2026-10-05T13:33:22Z |
+| consensus | override {difficulty_v2_activation_daa 33000, proving_v0_activation_daa 84100}, activation_height 84100, deadline_note "proving v0" | identical | identical |
+| fees_v1_activation_daa, tuning | absent | absent | absent |
+| mac | dmg 8ee96b3b..., 40,156,739 | dmg 53c12b6e..., 41,247,419, URL in its folder | same bytes, URL in the NEXT folder |
+| windows | inno-setup 2bacba64..., 19,784,297 | inno-setup 86cffd14..., 19,794,320, URL in its folder | same bytes, URL in the NEXT folder |
+
+`update-now-038` to all, published 14:00:39Z (both folders, the apps woken, stamp 2026-10-05T14:00:39Z.898f4a38).
+
+## 9. The machines after the publish (manifest live 13:33:20Z)
+
+Baseline 13:29:05Z: Mac d937c69d app 0.3.7 node 2b6d23ef DAA 104402 20.6 MH/s; PC 1 ae432dc7 0.3.7 DAA 104392 113.1 MH/s;
+PC 2 1ccfe586 0.3.7 DAA 104389 128.9 MH/s; PC 37ba0461 0.3.7 DAA 104376 2.3 MH/s; Sam's Mac 3a9bf309 silent 3 h (0.3.5).
+A machine counts as updated only when its node reports a DAA score and the miner a hash rate on 0.3.8.
+
+| Machine | On 0.3.8 | Its log (`IGNEUM-APP version=0.3.8`, fingerprints 477bb0ef key / ed9c4d2e folder on every one) |
+|---|---|---|
+| PC 1 ae432dc7 (Windows) | engine restart 13:42:01Z (unix 1791207722), 8 min 41 s after the manifest, on its own hourly slot (before the update-now job existed); node started 13:42:06Z, `node proof verifier reported: command` +6 s, `update to 0.3.8 complete (from 0.3.7)` 13:43:32Z; console 14:00Z: node 2b6d23ef, DAA 106190, 111.0 MH/s | run `win-ae432dc7-20261005-134201`: `[ok] updated to Igneum Miner 0.3.8 from 0.3.7`, `node proof verifier: command (...\igneum-prove-verify.exe)`, no `exited with code` line |
+| Mac d937c69d | engine restart 13:53:34Z (unix 1791208415), 20 min 14 s after the manifest, on its hourly slot; `ota-apply.log`: `apply: engine 99165 host 99158 ... version 0.3.8`, `staged bundle digest verified`, `swapped`, `0.3.8 is running` (13:53:29Z); `node proof verifier: command (/Applications/Igneum Miner.app/Contents/Resources/bin/igneum-prove-host)` and `reported: command` +10 s; `update to 0.3.8 complete` 13:55:05Z; console 14:00Z: node 2b6d23ef, DAA 106221, 22.0 MH/s | run `mac-d937c69d-20261005-135334`; the node log `node-20261005-135334.log`: `proving v0: payouts from DAA score 84100, window 7200 DAA, dust 5, verifier Command(".../igneum-prove-host")` |
+| PC 2 1ccfe586 (Windows) | `update-now-038` ran 14:01:12Z (33 s after the publish, woken); installer downloaded and verified 14:01:16Z; engine restart 14:01:22Z (unix 1791208882), 43 s after the publish; node started 14:01:26Z, synced at 106308 blocks with 3 peers at +1 s, `verifier reported: command` +7 s, 119.86 MH/s at +60 s; `update to 0.3.8 complete` 14:02:52Z; console 14:03:39Z: node 2b6d23ef, DAA 106429, 122.7 MH/s | run `win-1ccfe586-20261005-140122` |
+| PC 37ba0461 (Windows, the US laptop) | not reached: silent from 13:52:20Z (its last status line 13:51:51Z, 2.31 MH/s, up 3 h 26 min; its hourly slot is minute 57, the 13:57Z slot passed in silence). The update-now job and the manifest wait for it | run `win-37ba0461-20261005-102457` |
+| Sam's Mac 3a9bf309 | not reached: silent since 09:41Z (0.3.5) | |
+
+PC 2's 0.3.7 app and the jobs file: at 13:19:41Z PC 2 fetched the woken jobs file and logged `jobs: jobs file signature
+does not verify`; the same bytes re-signed at 13:23:04Z verified and ran. The file is canonical JSON signed by the same
+key; nothing in the signer or `jobs.rs` changed since 0.3.7. Unexplained (11).
+
+## 10. Cross-machine proving after the rollout
+
+The first cross-verified shard: block 68178 shard 0, proven on PC 2 (prover key e809e396..., payout address
+0xCAfc6e74...), `proven and submitted in 28 s` (assigned 14:05:37Z, submitted 14:06:05Z); the Mac's app node
+(`node-20261005-135334.log`): `proof record for block 68178 shard 0 by e809e396... (peer): accepted, verify Command(".../igneum-prove-host")`
+at 14:06:04.949Z, then `proof of block 68178 shard 0 by e809e396...: verified in 0.5 s: RESULT verify: VERIFIED in 0.052 s`
+at 14:06:05.638Z, then `chain block 68211: proof record for block 68178 shard 0 ... paid 868202990000000000 wei to 0xCAfc6e74...`
+at 14:06:08.782Z (PC 2's engine: `block 68178 shard 0 paid 0.86820299 IGN` at 14:06:15Z). Before the cut the same
+Mac host answered `rejected in 87.0 s: RESULT verify: NOT VERIFIED in 0.113 s` for every PC 2 record (the last at
+13:30:17Z): the whole point of 0.3.8, measured on the live devnet: a verify that took 87 to 88 s and failed now takes
+0.5 s and passes.
+
+Tally at 14:07:34Z (2 minutes after proving came back on): the Mac app node's pool `{entries 3, failed 0, pending 0,
+verified 3}`, paidShards 213 (211 before, both PCs' earlier records); the Mac node log since 14:04Z: 3 `VERIFIED in`,
+0 `NOT VERIFIED`, 3 `paid`. Blocks 68178, 68207 and 68235, each proven on PC 2 in 28 s, each verified on the Mac in
+0.5 s, 68178 and 68207 paid (0.8682 and 1.7366 IGN) when this was written. The live page (`/api/live`, fed by the
+observer): `pool {entries 3, verified 3, failed 0}`, `paid_shards_total 213`, `shards_proven_10m 3`, `shards_paid_10m 3`,
+and the block list shows the paid shard of block afdbec35 (DAA 106576) with prover e809e396, lag 39, payout 1.7365 IGN.
+So verified records and payouts are visible on the Mac's node and on the live page, not only on PC 2's node.
+
+Program id on each machine: Mac (installed app) `0x0dfade071ffc05a50be5f7e6640fb12638bac0ea63697ec252863f55658be16a`;
+PC 2 (`/opt/igneum`) the same; PC 1 has no prover (its node verifies through the wrapper's probe, which found no
+WSL host, so it reports `command` and trusts: not changed by this release); PC 37ba0461 has no WSL prover.
+
+## 11. Open after the cut
+
+| Item | State |
+|---|---|
+| PC 37ba0461 | silent from 13:52:20Z (before the update-now job); on 0.3.7; takes 0.3.8 on its next check (hourly slot minute 57) or when the job reaches it |
+| Sam's Mac 3a9bf309 | silent since 09:41Z, on 0.3.5; the same |
+| PC 2's 0.3.7 app refused the jobs file once (`jobs file signature does not verify`, 13:19:41Z) | the identical bytes re-signed verified four minutes later; the signer, the key and `jobs.rs` are unchanged since 0.3.7. Unexplained; if it recurs, diff the fetched bytes on the PC against the published file (a collect job on `jobs/` would show them) |
+| The pool was not observed empty before the Mac's host changed (section 7, step 2) | inferred from the 23 minutes between the last record and the restart; the watch that should have reported it treated a connection error as "not empty" and said nothing: the next watch of this kind prints every error line |
+| The package gate's execute half | skipped (`SKIP_GATE=1` on instruction); the native half ran on the DMG's host; PC 2's CUDA build proved three shards within two minutes, which is the stronger check |
+| PC 2's prover crates rebuilt in 5.65 s | plausible (three small crates, every dependency cached) and the installed host answers `--mode id`, which a 0.3.7 host cannot; not verified by a clean build |
+| Carried from 0.3.6/0.3.7 (section 10 there) | why the PC-built Windows node dies at start; the app marks "update complete" on its own health; the two finality tests under the parallel suite; igneumd not reproducible across PC 1 and PC 2 |
diff --git a/packaging/mac/app/Info.plist b/packaging/mac/app/Info.plist
index 890fe96ab..239ca2d10 100644
--- a/packaging/mac/app/Info.plist
+++ b/packaging/mac/app/Info.plist
@@ -11,7 +11,7 @@
CFBundleVersionVERSION_STAMPCFBundleShortVersionString
- 0.3.7
+ 0.3.8CFBundlePackageTypeAPPLCFBundleExecutable
diff --git a/packaging/windows/Igneum-Miner.iss b/packaging/windows/Igneum-Miner.iss
index 3855fbaf2..5afed4992 100644
--- a/packaging/windows/Igneum-Miner.iss
+++ b/packaging/windows/Igneum-Miner.iss
@@ -9,7 +9,7 @@
#define ArtDir "..\..\brand\icons"
#endif
#ifndef AppVersion
- #define AppVersion "0.3.7"
+ #define AppVersion "0.3.8"
#endif
#define AppName "Igneum Miner"
#define Publisher "Igneum"
diff --git a/site/bench.html b/site/bench.html
index ba8983136..ced6b28e2 100644
--- a/site/bench.html
+++ b/site/bench.html
@@ -171,12 +171,12 @@ th{font-family:var(--f-mono);font-size:12px;letter-spacing:.12em;text-transform:
-
64 entries, newest at the bottom
+
65 entries, newest at the bottom
Engineering log
Every measurement the project has made, newest at the bottom, written by the people and agents who ran it, with the commands and hardware. Prototype numbers are not mining numbers and say so.
-
+
Igneum bench log
Append-only. Every number here was measured on the machine named, on the date given.
Uncertain. (1) Every run is fast time (W = 120 DAA, ban 120, depth 20) on three nodes with 100-ms links; the mainnet values are 30 days, 30 days and 60 blocks. (2) The ban run shows one equivocation at one index; the red team's s1 (equivocation at every index, two keys) was re-run on the new build only through the red team's f23 above (0 refusals where the evening had 9 / 3 / 4). (3) The digest-less allowance on devnet and simnet is deliberate for the rollout and is a hole until removed. (4) The reorg run's final pass had the majority lock no index during the first 60 s of the split, so the re-determination at 8 and 9 was exercised, the pending-certificate path only at 10 and 11; the first pass exercised the opposite. (5) The un-determination rule has a unit test and one network pass (reorg-final2) in which the shallow-sink case did not recur, so the rule is exercised by the test, not by a run; the case needs a split whose difficulty drifts enough for blue work to overtake blue score, which happened once in four runs. (6) The red team's f24b is a window-length partition at 6 blocks/s, so it measures F21's stated limit, not F24; a 4/2 cut under 20 s at that rate would be the F24 case.
5 October 2026, live devnet: the first shards proven, verified and paid
Proving v0 activated at DAA 84,100 (manifest consensus.override, every node restarted with the same file; a hand node restarted early with another value was refused by the digest handshake and sat isolated for 20 minutes until it was restarted with the same file). The first proof records came from PC 2's RTX 5090 (SP1 CUDA under WSL2, app 0.3.7, node 2b6d23ef) and were verified by the Apple M5 Max node's verifier (igneum-prove-host --mode verify, the only block producer with a verifier until 0.3.7 put one on every machine) and paid at the carrying chain block.
-
What
Measured
First shard record in the pool (observer)
10:51 UTC, block 94,904 on the live page, prover key e809e396, shard 0, 0 pgas (an empty shard)
Paid shards by 10:53 UTC (Mac node igneum_getProvingStatus)
3 shards, 3.370437410 IGN in total, pool balance 68,601.72 IGN
not yet: the exporter's post-root assertion fires on blocks with content (58,584 to 58,984 on 5 October); investigation open
-
Commands: curl -X POST http://127.0.0.1:26800 -d '{"jsonrpc":"2.0","id":1,"method":"igneum_getProvingStatus","params":[]}' on the Apple M5 Max; node tools/logs.mjs for PC 2's prover lines (prover: block N shard 0 assigned to win-1ccfe586-1-1: export, cut, prove (CUDA), sign, submit).
+
What
Measured
First shard record in the pool (observer)
10:51 UTC, block 94,904 on the live page, prover key e809e396, shard 0, 0 pgas (an empty shard)
Paid shards by 10:53 UTC (Mac node igneum_getProvingStatus)
3 shards, 3.370437410 IGN in total, pool balance 68,601.72 IGN
not yet: the exporter's post-root assertion fires on blocks with content (58,584 to 58,984 on 5 October); investigation open
The assertion, explained (12:30 UTC, branch prover-match)
Not block content. Every block it fired on is empty (PC 2's export logs: 58,752 to 58,843 hit the assertion; 58,584 to 58,740 hit the backslash path of 6d51e53), one reward plus the pool credit, no transactions, no payouts. PC 2's exporter was a stale build: the panic names shard.rs:175, the line before commit 1251f0a moved the assert to 179, and that core's planner gave an empty segment the pre-root as its post-root while the statement applied the rewards (left = the node's root after the rewards, right = the root before them, as the log shows for 58,752). The core at master reproduces 58,927 and 59,192 with the node's roots, and the same shape (59,507: one reward to the same miner) was proven and paid after the 10:49 and 10:52 UTC rebuilds on PC 2. Branch prover-match: fixture block-58927-empty-reward.json, export/tests/fixtures.rs (every fixture reproduces; an empty segment ends at the root after the rewards), and a source stamp on the first line of the exporter and the host so a stale binary names itself. The guest is untouched: built in one directory, master and the branch give byte-identical loadable segments for the shard program and the aggregator (shard program id 0x1ec8b941 at master in that directory). Noted on the way: the same sources built in three directories on this Mac gave two different guest ELFs (text segment c173b3de in the main checkout and in a fresh worktree, 830f7433 in the branch's worktree, shard program id 0x366e2aca there), so the program id is not yet a pure function of the sources on a native build; SP1's docker build is the reproducible path and is not in use. Open item.
+
Commands: curl -X POST http://127.0.0.1:26800 -d '{"jsonrpc":"2.0","id":1,"method":"igneum_getProvingStatus","params":[]}' on the Apple M5 Max; node tools/logs.mjs for PC 2's prover lines (prover: block N shard 0 assigned to win-1ccfe586-1-1: export, cut, prove (CUDA), sign, submit).
+
5 October 2026, the program id split: why the Apple M5 Max rejected PC 2's proofs, and the verifier at 114 s
+
Machine: Apple M5 Max under the live devnet node, the Metal miner and two other agents' builds (every number here is wall time under that load, taken through the measure lock). Code: proving/igneum-prove on branch program-id, SP1 6.8.1, circuit v6.1.0.
app log run-20261004-r3-shards (node tools/logs.mjs job-collect-pc2-applog-paid-1ccfe586); confirmed by the sp1_vk_digest inside its proof of block 59507 shard 0 (below)
Three builds of the same guest sources, three ids. Cause: host/build.rs compiled the guest with sp1_build::build_program on whatever machine built the host, and the guest ELF depends on where it is built. Shown by strings on the two Mac ELFs: 946 anonymous symbol names differ, and the crate hash of igneum_prove_core is Csl6o96CsXEfN_ in the main checkout against Cs5Jl7brLd39a_ in the worktree (cargo's -C metadata for a path crate includes the checkout path, and rustc's symbol names carry it); the ELFs also embed ~/.cargo/registry/... panic-location strings, which differ again on Linux. A different ELF is a different verifying key, so every verifier rejects every other machine's proof ("sp1 vk hash mismatch" inside SP1's verify_compressed), and the node log showed it as a bare NOT VERIFIED after 114 s to 138 s. Over the same window the Apple M5 Max's pool read 16 entries, 9 failed, 0 verified, 22 shards paid (included by PC 2's own node).
+
Fix: the guests are pinned build artefacts (proving/igneum-prove/elf/: both ELFs, both verifying keys, manifest.json with SHA-256 hashes and ids), embedded by the host and checked at every start; --mode verify runs on SP1's light verifier with the pinned key, no prover client and no key setup; the verify line prints the id the proof was made with next to ours. Pinned set: shard 0x0dfade07...be16a, aggregator 0x135e67e7...6c62.
+
Verify of PC 2's proof of block 59507 shard 0 (1,272,897 bytes) on the Apple M5 Max
Setup
Verify
Verdict
Before: shipped host, ProverClient::from_env + two key setups
125.82 s
0.383 s
NOT VERIFIED, no reason given
Before, as the node saw it (blocks 59373 and 59402)
138.6 s and 114.4 s in all
0.409 s and 0.104 s
NOT VERIFIED
After: pinned key, light verifier (program-id host, same proof)
2.085 s
0.002 s (refused on the program id before any field arithmetic)
NOT VERIFIED, program id 0x05db1aca...61a3 IS NOT OURS 0x0dfade07...be16a; 2.35 s wall, exit 3
After, known-good case: block 56 shard 0 proven with the pinned ELF on this Mac (--mode compressed, 558,137 cycles, prove 1,066 s under load 113), verified against its real statement
1.323 s
0.108 s
VERIFIED, program id ... (ours); 1.80 s wall, exit 0
+
Before: 127.0 s wall per proof on the Apple M5 Max (the node saw 114 s to 139 s). After: 1.8 s to 2.4 s wall, under the 2 s target for the verify call itself; the remaining 1.3 s to 2.1 s is SP1's light verifier construction plus paging a 58 MB binary under load, and would shrink in a long-lived verifier process. Unit tests (cargo test -p igneum-prove-host --bin igneum-prove-host): the embedded files hash to the manifest, the embedded keys derive the manifest's ids, a changed file is refused; the ignored test re-runs SP1's setup on the embedded ELFs and gets the pinned ids. tools/ci/pinned-guests-check.sh was shown failing on an empty elf/ and passing on the pinned one.
+
What every machine must do: the pinned shard id 0x0dfade07...be16a differs from every id now running (Mac 0x0559759b..., PC 2 0x05db1aca...), so this is a guest change for the whole devnet, and proofs in flight at the switch are rejected by a verifier that has moved. Rollout order (proving/README.md, "Pinned guest programs"): provers off on every machine; wait until igneum_getProvingStatus shows an empty pool on every node; install the host built from this elf/ on every node (Mac DMG; PCs through igneum-prove-wsl2.zip, whose package carries elf/, so the WSL build embeds the same files); confirm igneum-prove-host --mode id prints the same shard id everywhere; provers back on. From then on a differing id is impossible without a change to the committed elf/.
Generated from the repository at build time. Times are UTC. Machine names are model names.