Merge branch 'release-0.3.8'
This commit is contained in:
commit
0f29fa4969
15 changed files with 584 additions and 40 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -67,5 +67,5 @@ jobs:
|
|||
run: bash tools/ci/pinned-guests-check.sh
|
||||
- name: relay unit tests (parsers, secret compare, the wake endpoint)
|
||||
run: node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs
|
||||
- name: miner app notice strip (ordering, keys, wording, timers)
|
||||
run: node --test app/igneum-app/ui/notices.test.mjs
|
||||
- name: miner app notice strip and update card (ordering, keys, wording, timers, when the card shows)
|
||||
run: node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs
|
||||
|
|
|
|||
2
app/igneum-app/Cargo.lock
generated
2
app/igneum-app/Cargo.lock
generated
|
|
@ -219,7 +219,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "igneum-app"
|
||||
version = "0.3.7"
|
||||
version = "0.3.8"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"getrandom",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "igneum-app"
|
||||
version = "0.3.7"
|
||||
version = "0.3.8"
|
||||
edition = "2021"
|
||||
description = "Igneum Miner engine: supervises the node, the miner and the GPU workers, and serves the dashboard on 127.0.0.1"
|
||||
license = "MIT"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
1 ICON "igneum.ico"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,3,7,0
|
||||
PRODUCTVERSION 0,3,7,0
|
||||
FILEVERSION 0,3,8,0
|
||||
PRODUCTVERSION 0,3,8,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
|
|
@ -20,12 +20,12 @@ BEGIN
|
|||
BEGIN
|
||||
VALUE "CompanyName", "Igneum"
|
||||
VALUE "FileDescription", "Igneum Miner engine"
|
||||
VALUE "FileVersion", "0.3.7"
|
||||
VALUE "FileVersion", "0.3.8"
|
||||
VALUE "InternalName", "igneum-app"
|
||||
VALUE "LegalCopyright", "Igneum contributors"
|
||||
VALUE "OriginalFilename", "igneum-app.exe"
|
||||
VALUE "ProductName", "Igneum Miner"
|
||||
VALUE "ProductVersion", "0.3.7"
|
||||
VALUE "ProductVersion", "0.3.8"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
|
|
|||
|
|
@ -299,6 +299,40 @@ td .sub{display:block;font-family:var(--mono);font-size:var(--t-xs);color:var(--
|
|||
.feed .k.block{color:var(--ember)}
|
||||
.feed .k.build{color:var(--ink-2)}
|
||||
|
||||
/* the update card (app.js, UpdateCard; the wallet carries the same block): the mark with its ring, the name, one
|
||||
line, up to three note lines, the size, Install now and Later. Over the key sheet and the settings panel. */
|
||||
.upd-wrap{position:fixed;inset:0;z-index:35;background:rgba(12,12,14,.72);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;animation:fade .25s ease}
|
||||
@keyframes fade{from{opacity:0}to{opacity:1}}
|
||||
.upd-card{position:relative;width:100%;max-width:500px;max-height:calc(100vh - 48px);overflow:auto;background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:34px 32px 28px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:var(--s-2);box-shadow:0 30px 80px rgba(0,0,0,.6),0 0 0 1px rgba(242,84,27,.08);animation:rise .3s ease;outline:none}
|
||||
.upd-card::before{content:"";position:absolute;left:50%;top:-80px;width:360px;height:260px;transform:translateX(-50%);border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.22) 0,rgba(242,84,27,0) 62%);pointer-events:none}
|
||||
.upd-mark{position:relative;width:96px;height:96px;display:flex;align-items:center;justify-content:center;margin-bottom:var(--s-3)}
|
||||
.upd-mark img{position:relative;display:block;filter:drop-shadow(0 6px 18px rgba(242,84,27,.35))}
|
||||
.upd-ring{position:absolute;inset:0;transform:rotate(-90deg)}
|
||||
.upd-ring .track{fill:none;stroke:var(--line-2);stroke-width:3}
|
||||
.upd-ring .arc{fill:none;stroke:var(--ember);stroke-width:3;stroke-linecap:round;stroke-dasharray:276.5;stroke-dashoffset:276.5;transition:stroke-dashoffset .4s linear,stroke .3s ease}
|
||||
.upd-mark.none .track{stroke:var(--line)}
|
||||
.upd-mark.full .arc{stroke:var(--molten);stroke-dashoffset:0}
|
||||
.upd-mark.busy .arc{stroke-dasharray:69 207.5;stroke-dashoffset:0;animation:spin 1.1s linear infinite;transform-origin:50% 50%}
|
||||
.upd-mark.failed .arc{stroke:var(--ember);stroke-dashoffset:0;opacity:.55}
|
||||
.upd-pct{position:absolute;left:50%;bottom:-11px;transform:translateX(-50%);font-size:var(--t-xs);font-weight:500;letter-spacing:.06em;color:var(--molten);background:var(--obsidian);border:1px solid var(--line-2);border-radius:999px;padding:2px 8px;font-variant-numeric:tabular-nums}
|
||||
.upd-name{font-size:24px;font-weight:700;letter-spacing:-.01em;position:relative}
|
||||
.upd-line{font-size:var(--t-xl);color:var(--ink-2);line-height:1.4;position:relative}
|
||||
.upd-cause{font-size:var(--t-sm);color:var(--ember);line-height:1.5;max-width:40ch;word-break:break-word}
|
||||
.upd-notes{list-style:none;margin:var(--s-2) auto 0;padding:0;width:max-content;max-width:100%;display:flex;flex-direction:column;gap:6px;align-items:flex-start;font-size:var(--t-md);color:var(--bone);line-height:1.4}
|
||||
.upd-notes:empty{display:none}
|
||||
.upd-notes li{display:flex;align-items:baseline;gap:10px;text-align:left}
|
||||
.upd-notes li::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--ember);flex:0 0 6px;position:relative;top:-2px}
|
||||
.upd-more{font:inherit;font-size:var(--t-sm);font-family:var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);background:transparent;border:0;padding:4px 8px;cursor:pointer;border-radius:6px;margin-top:2px}
|
||||
.upd-more:hover{color:var(--bone)}
|
||||
.upd-all{list-style:none;margin:0;padding:10px 14px;width:100%;max-height:150px;overflow:auto;text-align:left;font-size:var(--t-base);color:var(--ink-2);line-height:1.5;background:var(--obsidian);border:1px solid var(--line);border-radius:12px;display:flex;flex-direction:column;gap:4px;user-select:text;-webkit-user-select:text}
|
||||
.upd-meta{font-size:var(--t-sm);color:var(--ash);letter-spacing:.04em;margin-top:var(--s-2);min-height:18px;line-height:1.5;max-width:44ch}
|
||||
.upd-meta:empty{display:none}
|
||||
.upd-actions{display:flex;gap:var(--s-3);align-items:center;justify-content:center;flex-wrap:wrap;margin-top:var(--s-4);position:relative}
|
||||
.upd-actions:empty{display:none}
|
||||
.upd-actions .btn.primary{min-width:160px}
|
||||
@media (prefers-reduced-motion:reduce){.upd-wrap,.upd-card{animation:none}.upd-ring .arc{transition:none}.upd-mark.busy .arc{animation:none;stroke-dasharray:207.5 69}}
|
||||
@media (max-height:620px){.upd-card{padding:24px 24px 20px}.upd-mark{width:72px;height:72px;margin-bottom:var(--s-2)}.upd-mark img{width:32px;height:32px}.upd-name{font-size:20px}}
|
||||
|
||||
/* sheet (the key) */
|
||||
.sheet-wrap,.panel-wrap{position:fixed;inset:0;z-index:30;background:rgba(12,12,14,.72);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px}
|
||||
.sheet{background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:32px;max-width:640px;width:100%;max-height:calc(100vh - 48px);overflow:auto;display:flex;flex-direction:column;gap:14px;box-shadow:0 30px 80px rgba(0,0,0,.6);animation:rise .3s ease}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,110 @@ var Notices = (function () {
|
|||
}
|
||||
return { LEVEL: LEVEL, JOB_DONE_S: JOB_DONE_S, INSTALLED_S: INSTALLED_S, updateNotice: updateNotice, jobNotice: jobNotice, clockNotice: clockNotice, gather: gather, pick: pick, jobTitle: jobTitle, cap: cap, endDot: endDot };
|
||||
})();
|
||||
if (typeof module === 'object' && module && module.exports) module.exports = Notices;
|
||||
|
||||
/* ---------- the update card (pure; update-card.test.mjs loads this block) ----------
|
||||
A centred card over the window for one update: the mark with a progress ring, "Igneum Ember 0.3.7", one line
|
||||
(is available, is downloading, is ready to install, Installing, did not install), up to three lines of release
|
||||
notes with the rest behind "What changed", the size, Install now and Later. The same card, the same rules and the
|
||||
same words sit in the wallet (app/igneum-wallet/ui/update-card.js).
|
||||
model() turns state.update into what the card says. decide() says whether it shows now, given the window
|
||||
(ctx) and what the card already did (mem: open, later, seen):
|
||||
deferred while a job runs, in the engine's first 60 s, while the key sheet is up, while the app quits
|
||||
later Later, Escape or the backdrop hides this version at this stage; the strip (Notices) keeps it
|
||||
back a newer version, or the download ready while automatic updates are off (with them on it installs
|
||||
by itself, so a dismissed download stays dismissed); an open card follows its update to the end
|
||||
installing and failed show only on an open card (Install now was pressed here); the strip carries the rest */
|
||||
var UpdateCard = (function () {
|
||||
'use strict';
|
||||
var NAME = 'Igneum Ember', LINE_MAX = 70, LINES = 3, FIRST_S = 60;
|
||||
var INSTALL = { act: 'install', label: 'Install now', primary: true }, LATER = { act: 'later', label: 'Later' };
|
||||
var RETRY = { act: 'retry', label: 'Try again', primary: true }, OPEN = { act: 'open', label: 'Open the download', primary: true };
|
||||
function cap(t) { return t ? t.charAt(0).toUpperCase() + t.slice(1) : ''; }
|
||||
function firstLine(t, max) { t = String(t || '').split('\n')[0].trim(); max = max || 160; return t.length > max ? t.slice(0, max - 1).trim() + '…' : t; }
|
||||
// the manifest's notes as sentences: split on line breaks, then after . ! ? ; (no lookbehind: the Mac WebView)
|
||||
function sentences(text) {
|
||||
var out = [], parts = String(text || '').split(/\n+/);
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var p = parts[i], at = 0;
|
||||
for (var j = 0; j < p.length; j++) {
|
||||
var ch = p.charAt(j), next = p.charAt(j + 1);
|
||||
if ((ch === '.' || ch === '!' || ch === '?' || ch === ';') && (next === ' ' || next === '')) { push(p.slice(at, j + 1)); at = j + 1; }
|
||||
}
|
||||
push(p.slice(at));
|
||||
}
|
||||
function push(s) { s = s.trim().replace(/[;:,]+$/, ''); if (s) out.push(cap(s)); }
|
||||
return out;
|
||||
}
|
||||
// at most LINES lines, each under LINE_MAX characters (cut at a word); more = something is left for "What changed"
|
||||
function splitNotes(text) {
|
||||
var all = sentences(text), lines = [], more = all.length > LINES;
|
||||
for (var i = 0; i < all.length && lines.length < LINES; i++) {
|
||||
var s = all[i].replace(/\.$/, '');
|
||||
if (s.length >= LINE_MAX) { var cut = s.lastIndexOf(' ', LINE_MAX - 2); s = s.slice(0, cut > 20 ? cut : LINE_MAX - 2).replace(/[,;:]$/, '') + '…'; more = true; }
|
||||
lines.push(s);
|
||||
}
|
||||
return { lines: lines, more: more, all: all };
|
||||
}
|
||||
function size(bytes) { if (!(bytes > 0)) return ''; return bytes < 1e6 ? (bytes / 1e6).toFixed(1) + ' MB' : Math.round(bytes / 1e6) + ' MB'; }
|
||||
function card(stage, u, over) {
|
||||
var ver = u.version || '';
|
||||
var m = { stage: stage, version: ver, key: 'update:' + ver + ':' + (stage === 'available' || stage === 'downloading' || stage === 'staging' ? 'pending' : stage), name: NAME + ' ' + ver, line: '', note: '', cause: '', size: size(u.size), pct: -1, ring: 'none', actions: [], dismissable: true, auto: !!u.auto };
|
||||
var n = splitNotes(u.notes); m.lines = n.lines; m.more = n.more; m.all = n.all;
|
||||
if (over) for (var k in over) m[k] = over[k];
|
||||
return m;
|
||||
}
|
||||
// u = state.update; s = { version, uptime_s, quitting }
|
||||
function model(u, s) {
|
||||
if (!u || !u.version) return null;
|
||||
s = s || {};
|
||||
var pct = u.progress > 0 ? Math.min(100, Math.round(u.progress * 100)) : 0;
|
||||
if (u.status === 'error') {
|
||||
if (/no update manifest configured/.test(u.error || '')) return null;
|
||||
return card('failed', u, { line: u.rolled_back ? 'did not stay up and was rolled back.' : 'did not install.', cause: firstLine(u.error, 120), ring: 'failed', actions: [RETRY, LATER], lines: [], more: false });
|
||||
}
|
||||
if (u.applying || u.status === 'applying' || (u.status === 'ready' && u.wait === 'installing now')) {
|
||||
return card('installing', u, { line: 'Installing. The app restarts itself.', note: s.quitting ? 'A moment.' : 'Mining continues until then.', ring: 'busy', dismissable: false, lines: [], more: false });
|
||||
}
|
||||
var m = null;
|
||||
switch (u.status) {
|
||||
case 'available': m = card('available', u, { line: 'is available.', actions: [INSTALL, LATER] }); break;
|
||||
case 'downloading': m = card('downloading', u, { line: 'is downloading.', pct: pct, ring: 'progress', actions: [INSTALL, LATER] }); break;
|
||||
case 'staging': m = card('staging', u, { line: 'is being checked.', pct: 100, ring: 'progress', actions: [INSTALL, LATER] }); break;
|
||||
case 'ready':
|
||||
var why = /failed to install before/.test(u.wait || '') ? 'It failed to install before.' : u.auto ? 'It installs by itself at a quiet moment.' : '';
|
||||
m = card('ready', u, { line: 'is ready to install.', note: why, ring: 'full', actions: [INSTALL, LATER] }); break;
|
||||
case 'deferred': m = card('deferred', u, { line: 'is waiting for permission.', note: 'It installs the next time someone is at this PC. Mining continues.', ring: 'full', actions: [INSTALL, LATER] }); break;
|
||||
case 'manual': m = card('manual', u, { line: 'is downloaded.', note: 'Open the disk image and drag the app over the old one.', ring: 'full', actions: [OPEN, LATER] }); break;
|
||||
}
|
||||
if (!m) return null;
|
||||
if (u.urgent && u.urgent_text) { m.note = u.urgent_text; m.dismissable = false; m.actions = m.actions.filter(function (a) { return a.act !== 'later'; }); }
|
||||
return m;
|
||||
}
|
||||
// why the card may not open now: ctx = { jobActive, uptime_s, quitting, sheetOpen }
|
||||
function blocked(ctx) {
|
||||
ctx = ctx || {};
|
||||
if (ctx.quitting) return 'the app is quitting';
|
||||
if (ctx.sheetOpen) return 'the key sheet is open';
|
||||
if (ctx.jobActive) return 'a job is running';
|
||||
if (ctx.uptime_s >= 0 && ctx.uptime_s < FIRST_S) return 'the engine started under a minute ago';
|
||||
return '';
|
||||
}
|
||||
// mem = { open: the card is up, later: the key Later was pressed on, seen: the version the card was shown for }
|
||||
function decide(m, ctx, mem) {
|
||||
mem = mem || {};
|
||||
if (!m) return { show: false, why: 'none' };
|
||||
var b = blocked(ctx);
|
||||
if (b && m.stage !== 'installing') return { show: false, why: 'deferred', reason: b };
|
||||
if (m.stage === 'installing' || m.stage === 'failed') return mem.open ? { show: true, why: 'open' } : { show: false, why: 'strip' };
|
||||
if (!m.dismissable) return { show: true, why: 'urgent' };
|
||||
if (mem.later === m.key) return { show: false, why: 'later' };
|
||||
if (mem.open) return { show: true, why: 'open' };
|
||||
if (m.stage === 'ready' && m.auto && mem.seen === m.version) return { show: false, why: 'auto' };
|
||||
return { show: true, why: m.stage === 'ready' ? 'ready' : 'new' };
|
||||
}
|
||||
return { NAME: NAME, LINE_MAX: LINE_MAX, LINES: LINES, FIRST_S: FIRST_S, sentences: sentences, splitNotes: splitNotes, size: size, model: model, blocked: blocked, decide: decide };
|
||||
})();
|
||||
if (typeof module === 'object' && module && module.exports) { module.exports = Notices; module.exports.UpdateCard = UpdateCard; }
|
||||
|
||||
if (typeof document !== 'undefined') (function () {
|
||||
'use strict';
|
||||
|
|
@ -851,6 +954,63 @@ if (typeof document !== 'undefined') (function () {
|
|||
else if (act === 'sync') api('api/clock/sync', {});
|
||||
});
|
||||
|
||||
// ---------- the update card (UpdateCard, top of this file): one update, centred, over everything ----------
|
||||
// mem: open (the card is up), later (the key Later was pressed on), seen (the version it was shown for);
|
||||
// asked: Install now was pressed on this card and the engine has not flipped to applying yet
|
||||
var updMem = { open: false, later: '', seen: '' }, updSig = '', updAsked = '', updMore = false;
|
||||
if (forcedUpdate && params.get('card') === '1') updMem.open = true; // screenshots: installing and failed only show on an open card
|
||||
function cardCtx(s) { return { jobActive: !!(s.jobs && s.jobs.active), uptime_s: s.uptime_s, quitting: !!s.quitting, sheetOpen: !$('sheet-key').hidden }; }
|
||||
function renderUpdateCard(s) {
|
||||
var m = UpdateCard.model(s.update, s), d = UpdateCard.decide(m, cardCtx(s), updMem), wrap = $('upd');
|
||||
if (!d.show) {
|
||||
if (updMem.open || !wrap.hidden) { updMem.open = false; wrap.hidden = true; updSig = ''; }
|
||||
return;
|
||||
}
|
||||
if (!updMem.open) { updMem.open = true; updMem.seen = m.version; updMore = false; if (updAsked !== m.key) updAsked = ''; }
|
||||
var asked = updAsked === m.key && m.stage !== 'installing' && m.stage !== 'failed';
|
||||
var sig = [m.key, m.stage, m.line, m.note, m.cause, m.ring, asked ? 'asked' : '', m.lines.join('|')].join('\u0001');
|
||||
if (sig !== updSig) {
|
||||
updSig = sig;
|
||||
$('upd-name').textContent = m.name;
|
||||
$('upd-line').textContent = m.line;
|
||||
$('upd-cause').textContent = m.cause; $('upd-cause').hidden = !m.cause;
|
||||
$('upd-notes').innerHTML = m.lines.map(function (l) { return '<li>' + esc(l) + '</li>'; }).join('');
|
||||
$('upd-all').innerHTML = m.all.map(function (l) { return '<li>' + esc(l) + '</li>'; }).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 '<button class="btn ' + (a.primary ? 'primary big' : 'ghost') + '" data-act="' + esc(a.act) + '"' + (a.disabled ? ' disabled' : '') + '>' + esc(a.label) + '</button>'; }).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=<kind>[&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; }
|
||||
|
|
|
|||
|
|
@ -218,6 +218,26 @@
|
|||
</section>
|
||||
</main>
|
||||
|
||||
<!-- the update card (app.js, UpdateCard): one update, centred; Later, Escape or the backdrop leaves the strip above -->
|
||||
<div class="upd-wrap" id="upd" hidden>
|
||||
<div class="upd-card" id="upd-card" role="dialog" aria-modal="true" aria-labelledby="upd-name" aria-describedby="upd-line" tabindex="-1">
|
||||
<div class="upd-mark" id="upd-mark">
|
||||
<svg class="upd-ring" viewBox="0 0 96 96" aria-hidden="true"><circle class="track" cx="48" cy="48" r="44"></circle><circle class="arc" id="upd-arc" cx="48" cy="48" r="44"></circle></svg>
|
||||
<img src="mark.svg" width="40" height="40" alt="">
|
||||
<span class="upd-pct mono" id="upd-pct" hidden></span>
|
||||
</div>
|
||||
<div class="eyebrow ember">update</div>
|
||||
<h2 class="upd-name" id="upd-name"></h2>
|
||||
<p class="upd-line" id="upd-line"></p>
|
||||
<p class="upd-cause mono" id="upd-cause" hidden></p>
|
||||
<ul class="upd-notes" id="upd-notes"></ul>
|
||||
<button class="upd-more" id="upd-more" type="button" aria-expanded="false" hidden>What changed</button>
|
||||
<ul class="upd-all" id="upd-all" hidden></ul>
|
||||
<p class="upd-meta mono" id="upd-meta"></p>
|
||||
<div class="upd-actions" id="upd-actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- the key sheet, shown once -->
|
||||
<div class="sheet-wrap" id="sheet-key" hidden>
|
||||
<div class="sheet">
|
||||
|
|
|
|||
118
app/igneum-app/ui/update-card.test.mjs
Normal file
118
app/igneum-app/ui/update-card.test.mjs
Normal file
|
|
@ -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' });
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
199
docs/plans/release-0.3.8.md
Normal file
199
docs/plans/release-0.3.8.md
Normal file
|
|
@ -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=<main>/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 <worktree>/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=<main>/vendor/igneum-node-036/target-integration/x86_64-pc-windows-gnu/release IGNEUM_NODE_SRC=<main>/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 <scratch>/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 <app.url>/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 <app.url>/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 |
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
<key>CFBundleVersion</key>
|
||||
<string>VERSION_STAMP</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.7</string>
|
||||
<string>0.3.8</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -50,6 +50,11 @@
|
|||
}
|
||||
],
|
||||
"log": [
|
||||
{
|
||||
"date": "2026-10-05",
|
||||
"text": "The program id split: why the Apple M5 Max rejected PC 2's proofs, and the verifier at 114 s",
|
||||
"short": "The program id split"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-05",
|
||||
"text": "Live devnet: the first shards proven, verified and paid",
|
||||
|
|
@ -217,38 +222,33 @@
|
|||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "RTX 5090 first run",
|
||||
"short": "RTX 5090 first run"
|
||||
"text": "RTX 5090, memory-hard dataset",
|
||||
"short": "RTX 5090 on the memory-hard dataset"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "AMD gfx1036 , AMD OpenCL 2.1 driver 3652.0",
|
||||
"short": "AMD integrated GPU runs the hash through OpenCL"
|
||||
"text": "Proto-opencl: OpenCL path built and proven without AMD silicon",
|
||||
"short": "OpenCL worker built and proven without AMD silicon"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "First devnet blocks on the real lottery hash: CPU, then Metal GPU, three worker implementations",
|
||||
"short": "First devnet blocks on the real lottery hash"
|
||||
"text": "RTX 5090 through NVIDIA OpenCL",
|
||||
"short": "RTX 5090 through NVIDIA OpenCL"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "R3.26 / M15: PoW checked after the cheap checks, cache-build cap, attack before and after",
|
||||
"short": "Cache-build attack closed: 10.6 s of rebuilds to 14 ms"
|
||||
"text": "Igneum-node devnet v0: 3-node igneum-devnet at 1 BPS with the 80/20 coinbase and vote_key_hash",
|
||||
"short": "Devnet v0: three nodes at one block per second"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "Difficulty controller: devnet record, simulator, Igneum dual-lane rule, 3-node CPU test network",
|
||||
"short": "Igneum dual-lane difficulty rule built and simulated"
|
||||
"text": "Windows node package: igneumd cross-compiled for x86_64-pc-windows-gnu, two-peer sync test",
|
||||
"short": "Windows node package: cross-compiled, two-peer sync"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "Weak-program census: 400,000 program runs through the CPU reference, the redundant-load finding, and the rules for M5 and M6",
|
||||
"short": "Census of 400,000 programs: redundant loads found"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "Proving v0: first SP1 proof of an Igneum block, Apple M5 Max CPU, loaded machine",
|
||||
"short": "First SP1 proof of an Igneum block, on a laptop CPU"
|
||||
"text": "Igneum-node devnet v2: sustained-mining finality rule v2 on a four-miner test network, and as a follower of the live devnet",
|
||||
"short": "Finality rule v2 live on a four-miner test network"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue