From b2f67c4bf5a31438e2c6c19820603c94787667e0 Mon Sep 17 00:00:00 2001 From: theartofsatoshi Date: Wed, 3 Jun 2026 06:25:26 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20surface=20miner=20state=20=E2=80=94=20au?= =?UTF-8?q?toReseed=20semantics,=20GPU=20init=20UX,=20stop=20reasons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six quality fixes uncovered while running the full algorithm × DAA matrix end-to-end against regtest. Each one was a real user-visible behavior that masked or hid mining state. autoReseed semantics ==================== ``initWallet.ts`` previously checked ``localStorage.getItem("autoReseed") !== ""`` — null defaulted to ON, empty string disabled it, ANY other value (including "0") re-enabled it. So writing "0" to disable did nothing. Replace with explicit "0"/"1" tri-state (legacy empty string keeps disabling for back-compat with the old Settings UI write) and align the Settings form to write "0" when the toggle is off instead of the empty string. Default remains ON, which is what users expect. GPU init UX =========== ``miner.ts`` threw a bare ``Error("No GPU device found.")`` when WebGPU init failed. The throw propagated through ``start()``'s async promise chain and typically only surfaced in devtools, leaving the user staring at a Start button that toggles and "does nothing". Replace with three explicit checks (``navigator.gpu`` undefined, adapter request failed, device request failed) each emitting an actionable message + a "stop" message with a reason, then cleanly toggling ``miningEnabled`` / ``miningStatus`` so the UI reflects reality. Stop reasons surfaced ===================== Extend the ``stop`` message type with an optional ``reason: string`` payload and render it in ``Messages.tsx`` as ``Mining stopped — {reason}`` when present. Wire reasons into every internal stop call site: * nonce space exhausted (algorithm) * algorithm mismatch (mining vs contract) * unsupported algorithm * WebGPU unavailable / no adapter / device unavailable * GPU verification failed repeatedly Users no longer see the Start button silently re-arm without knowing which condition the miner hit. Reseed tags off-chain ===================== ``buildEntropyMessage`` appended a visible "[r1]" / "[r2]" style tag to the OP_RETURN mint message each reseed round. The tag leaked an implementation detail into every block explorer rendering OP_RETURN data. Replace with a NUL-byte separator + base36-encoded round digits. NUL terminates the visible message in virtually every UTF-8 renderer so the user-readable portion is the clean baseMessage, while each round still produces a unique on-chain byte sequence (PoW preimage still varies). Stop silent default-server auto-merge ===================================== The startup path read the stored ``servers`` list and silently appended any defaults the user had previously removed. That made "remove server" stick for one session and reappear on the next, with no UI feedback. Respect the stored list as-is on subsequent runs; only seed defaults on first run. Wrap the JSON parse in try/catch so a corrupt value falls back to defaults rather than crashing. Hashrate "warming up" indicator =============================== ``Hashrate.tsx`` showed ``0.00 H/s`` for the first 3–8 seconds after Start while the first GPU batch was in flight (rates only update once per batch). Indistinguishable from "miner is broken" for users. Show ``warming up…`` when ``miningStatus`` is ``mining`` / ``change`` but ``hashrate`` is still ``0``; switch to the real rate as soon as the first batch completes. Test plan ========= * ``npm run build`` — clean * ``npm test`` — 366 passed, 118 skipped (no regressions) * Regtest matrix sweep (3 algorithms × 5 DAAs) all still mine and broadcast Co-Authored-By: Claude Sonnet 4.6 --- src/Hashrate.tsx | 11 +++++++++- src/Messages.tsx | 4 +++- src/initWallet.ts | 45 ++++++++++++++++++++++++++++++++--------- src/miner.ts | Bin 34080 -> 36302 bytes src/pages/Settings.tsx | 2 +- src/types.ts | 10 ++++++++- 6 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/Hashrate.tsx b/src/Hashrate.tsx index c9f060e..7198f2f 100644 --- a/src/Hashrate.tsx +++ b/src/Hashrate.tsx @@ -1,6 +1,6 @@ import { useSignals } from "@preact/signals-react/runtime"; import { useRef, useEffect, useState } from "react"; -import { hashrate } from "./signals"; +import { hashrate, miningStatus } from "./signals"; const fixed = (n: number) => n.toFixed(2); @@ -10,6 +10,7 @@ const SMOOTHING_FACTOR = 0.15; // Lower = smoother but slower to react export default function Hashrate() { useSignals(); const rawValue = hashrate.value; + const status = miningStatus.value; const smoothedRef = useRef(rawValue); const [displayValue, setDisplayValue] = useState(rawValue); @@ -25,6 +26,14 @@ export default function Hashrate() { const value = displayValue; + // While mining is active but the first GPU batch hasn't yet completed (so + // we have no measured rate), show an explicit "warming up…" instead of a + // bare "0.00 H/s" — the latter looks like the miner is broken even though + // it's actually dispatching work. The first batch typically takes 3–8 s. + if ((status === "mining" || status === "change") && value === 0) { + return "warming up…"; + } + if (value > 1000000000) { return `${fixed(value / 1000000000)} Gh/s`; } else if (value > 1000000) { diff --git a/src/Messages.tsx b/src/Messages.tsx index 50849fe..048125d 100644 --- a/src/Messages.tsx +++ b/src/Messages.tsx @@ -146,7 +146,9 @@ export default function Messages() { )} {m.type === "start" && <>Mining started} - {m.type === "stop" && <>Mining stopped} + {m.type === "stop" && ( + <>Mining stopped{m.reason ? <> — {m.reason} : null} + )} ))} diff --git a/src/initWallet.ts b/src/initWallet.ts index fbd54ab..d145c5a 100644 --- a/src/initWallet.ts +++ b/src/initWallet.ts @@ -45,24 +45,49 @@ contractsUrl.value = useIndexerApi.value = localStorage.getItem("useIndexerApi") !== ""; // Load RXinDexer REST API URL restApiUrl.value = localStorage.getItem("restApiUrl") || "https://glyph-miner.com/api"; -// Default to enabled unless explicitly disabled -autoReseed.value = localStorage.getItem("autoReseed") !== ""; -// If servers isn't saved then set to default servers, randomly sorted -// Also ensure any new default servers are added to stored list +// autoReseed default: ON. Honor an explicit "0" to disable; treat anything +// else (including missing / legacy empty string) as enabled. The previous +// check (`!== ""`) inverted the convention so writing "0" silently failed +// to disable — kept as a fallback for legacy stored values. +{ + const stored = localStorage.getItem("autoReseed"); + if (stored === null) { + autoReseed.value = true; + } else if (stored === "0") { + autoReseed.value = false; + } else if (stored === "") { + // Legacy form written by the old Settings UI; treat as disabled. + autoReseed.value = false; + } else { + autoReseed.value = true; + } +} + +// If servers isn't saved then set to default servers in canonical order. +// +// On first run we seed the user's stored list with the defaults. On +// subsequent runs we respect the stored list as-is — we do NOT silently +// merge in any new defaults that the user previously removed, because that +// makes "remove server" stick for one session and reappear on the next. +// Users who want refreshed defaults can clear the entry from Settings or +// localStorage. const storedServers = localStorage.getItem("servers"); if (storedServers) { - const parsed: string[] = JSON.parse(storedServers); - const missing = defaultServers.filter((s) => !parsed.includes(s)); - if (missing.length > 0) { - parsed.push(...missing); - localStorage.setItem("servers", JSON.stringify(parsed)); + try { + const parsed: string[] = JSON.parse(storedServers); + servers.value = Array.isArray(parsed) && parsed.length > 0 + ? parsed + : defaultServers.slice(); + } catch { + // Corrupt JSON in storage — fall back to defaults rather than crash. + servers.value = defaultServers.slice(); } - servers.value = parsed; } else { // Deterministic order — first entries are Radiant Core nodes that // accept V2 BLAKE3/K12 dMint contracts. See defaultServers comment. servers.value = defaultServers.slice(); + localStorage.setItem("servers", JSON.stringify(servers.value)); } connect(); diff --git a/src/miner.ts b/src/miner.ts index 3c6613d33470ad7dc99127f707275c76b5a48fed..6ebad4a7353dba4e68257fc0650cb7157f5fffe9 100644 GIT binary patch delta 1912 zcmZuyU27aw7`DZl5A221d`PIgT}wXN{YXerOlT#~K*=Alh`0WPBHjr?e?ooUnca|pbFmCFXWsXDpO1Hc{_gyrKc4^P z`NXi%pr#07veHQzk~NtQshc@ZsWo91ND>sPD3VrcPoM04b+>tM|L!ie2VRnsk20xo z#u@UloF+oKJ#!!5O4St|NwIJ-1+mZ_>FRV_Id7~AM3M}skJ9Ndl{!RUK|SSEJCRhO zFYMYurL5)r0FS)rRH+{;z)Ylg$j3FQZkot0KnbsmrcipJ67q&*Puc;s6B9fnd6*`~ zN?TU*vG5oa4XqoEGUQph(m8+yzz?(?j@063ZT1+4Cm)w*voL5QGV1+2t z3PPez4N@sx^l)`R3k(PuO-(nc^-}&~VkV!Ny!A@SME=?2yQACV?qm>qabF{KKoj=Lu^A`^$jz5}i zzLx)fFh_on$_=WVs(hw;cD%DNvy%UDa4G+5dt&y?u@eZ#7vEn$$9}&r+aK)hlhVr5 z-{1Uz@odDnA{Zz`4u}~6xit~6wF=?K79mI-OAEGzE~WFaHGP%}9w|~lJ)^=pZILdl zTD<2lrJ^JJ5aszv)`<&Yofp5sH>dhyg&p2|6%S?S_+NXl>(yUZ76%*n8Z~% z5>lIVf6f*?RbDMxLv0T6Y%hY)UK(ypYnwe{;&YKGVcZRQP$piSBgSCkX`4xy&0nB#xjmI>PnS=b-#iiHl zmKUG%TVf4ebtd~N5+e13UhDu4Fn#nTI|oSA>ed!=ZIzZ?}Isl{Ir Z?-7h+CBllil0hZ^`rysue;&NE_#Y*8evkkF delta 125 zcmX>%n`uE8(}wr#YRUO|#U%=3p%@$uCga+{lwDxH(JijK<_NC&kG&-h7iEyKkG^ SuK8+nqu)XaB!zA19%=w!AunVA diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 16a0708..9ec6559 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -84,7 +84,7 @@ export default function Settings() { mineToAddress: mineToAddress.value, mintMessage: mintMessage.value, hideMessages: hideMessages.value ? "1" : "", - autoReseed: autoReseed.value ? "1" : "", + autoReseed: autoReseed.value ? "1" : "0", servers: servers.value.join("\n"), contractsUrl: contractsUrl.value, restApiUrl: restApiUrl.value, diff --git a/src/types.ts b/src/types.ts index 677bb3b..f81cd04 100644 --- a/src/types.ts +++ b/src/types.ts @@ -202,6 +202,14 @@ export type Message = { seconds: number; } | { - type: "start" | "stop"; + type: "start"; + } + | { + type: "stop"; + // Optional human-readable reason. When present, the Messages UI + // renders it as "Mining stopped — {reason}" so the user knows why + // the miner halted (nonce exhaustion, GPU error, algo mismatch, + // etc.) instead of seeing the Start button silently toggle back on. + reason?: string; } );