UNPKG

nano-pow

Version:

Proof-of-work generation and validation with WebGPU/WebGL/WASM for Nano cryptocurrency.

510 lines (494 loc) 15.4 kB
#!/usr/bin/env node // src/bin/cli.ts import { spawn } from "node:child_process"; import { getRandomValues } from "node:crypto"; import { createInterface } from "node:readline/promises"; // src/utils/api-support/wasm.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later var wasm = { isSupported: false }; Object.defineProperty(wasm, "isSupported", { get: async function() { let isWasmSupported = false; try { const wasmBuffer = new Uint8Array([ 0, 97, 115, 109, 1, 0, 0, 0, // WASM magic header and version 1, 4, // Type section, 4 byte descriptor 1, 96, 0, 0, // 1 type, type function, 0 params, 0 returns 3, 2, // Function section, 2 byte descriptor 1, 0, // 1 function, ID 0 10, 23, // Code section, 23 bytes 1, 21, // 1 function body, 21 bytes 0, // 0 local variables 253, 12, // v128.const 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // assign i64x2 values 26, 11 // Drop result, end function ]); const module = await WebAssembly.compile(wasmBuffer); const instance = await WebAssembly.instantiate(module); isWasmSupported = instance instanceof WebAssembly.Instance; } catch (err) { console.warn("WASM is not supported in this environment.\n", err.message ?? err); isWasmSupported = false; } finally { delete this.isSupported; this.isSupported = isWasmSupported; return this.isSupported; } } }); // src/utils/api-support/webgl.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later var webgl = { isSupported: false }; Object.defineProperty(webgl, "isSupported", { get: async function() { let isWebglSupported = false; try { const gl = new OffscreenCanvas(0, 0)?.getContext?.("webgl2"); isWebglSupported = gl instanceof WebGL2RenderingContext; } catch (err) { console.warn("WebGL is not supported in this environment.\n", err); isWebglSupported = false; } finally { delete this.isSupported; this.isSupported = isWebglSupported; return this.isSupported; } } }); // src/utils/api-support/webgpu.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later var webgpu = { isSupported: false }; Object.defineProperty(webgpu, "isSupported", { get: async function() { let isWebgpuSupported = false; try { const adapter = await navigator?.gpu?.requestAdapter?.(); isWebgpuSupported = adapter instanceof GPUAdapter; } catch (err) { console.warn("WebGPU is not supported in this environment.\n", err); isWebgpuSupported = false; } finally { delete this.isSupported; this.isSupported = isWebgpuSupported; return this.isSupported; } } }); // src/utils/api-support/index.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later // src/utils/bigint.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later // src/utils/cache.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later // src/utils/logger.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later var Logger = class { isEnabled = false; groups = {}; groupStart(name) { if (this.isEnabled) { console.groupCollapsed(name); this.groups[name] = true; } } groupEnd(name) { if (this.groups[name]) { console.groupEnd(); delete this.groups[name]; } } log(...args2) { if (this.isEnabled) { const datetime = new Date(Date.now()).toLocaleString(Intl.DateTimeFormat().resolvedOptions().locale ?? "en-US", { hour12: false, dateStyle: "medium", timeStyle: "medium" }); for (let i = 0; i < args2.length; i++) { if (typeof args2[i] === "string") { args2[i] = args2[i].replace(datetime, "").trimStart(); } if (args2[i] instanceof Error) { if ("stack" in args2[i]) { args2.splice(i + 1, 0, args2[i].stack); } else if ("message" in args2[i]) { args2.splice(i + 1, 0, args2[i].message); } } } const entry = `${datetime} ${globalThis.process?.title ?? "NanoPow"}[${globalThis.process?.pid ?? "browser"}]:`; console.log(entry, ...args2); globalThis.process?.send?.({ type: "console", data: `${entry} ${args2}` }); } } }; // src/utils/queue.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later // src/utils/index.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later function isHexN(value, byteLength) { if (typeof value !== "string") return false; const v = value.replace(/^0[Xx]/, ""); const length = byteLength === null ? "+" : `{${2 * byteLength}}`; const r = new RegExp(`^[A-Fa-f\\d]${length}$`); return r.test(v); } function isHex8(value) { return isHexN(value, 8); } function isHex32(value) { return isHexN(value, 32); } function stats(times) { if (times == null || times.length === 0) return null; const count = times.length; const truncatedStart = Math.floor(count * 0.1); const truncatedEnd = count - truncatedStart; const truncatedCount = truncatedEnd - truncatedStart; let min = Number.MAX_SAFE_INTEGER; let logarithms, max, median, reciprocals, total; logarithms = max = median = reciprocals = total = 0; let truncatedMin = Number.MAX_SAFE_INTEGER; let truncatedLogarithms, truncatedMax, truncatedReciprocals, truncatedTotal; truncatedLogarithms = truncatedMax = truncatedReciprocals = truncatedTotal = 0; times.sort((a, b) => a - b); for (let i = 0; i < count; i++) { const time = times[i]; total += time; logarithms += Math.log(time); reciprocals += 1 / time; min = Math.min(min, time); max = Math.max(max, time); if (i === Math.floor((count - 1) / 2)) median = time; if (i === Math.floor(count / 2) && count % 2 === 0) median = (median + time) / 2; } for (let i = truncatedStart; i < truncatedEnd; i++) { const time = times[i]; truncatedTotal += time; truncatedLogarithms += Math.log(time); truncatedReciprocals += 1 / time; truncatedMin = Math.min(truncatedMin, time); truncatedMax = Math.max(truncatedMax, time); } return { count, total, rate: 1e3 * count / total, min, max, median, arithmetic: total / count, geometric: Math.exp(logarithms / count), harmonic: count / reciprocals, truncatedCount, truncatedTotal, truncatedRate: 1e3 * truncatedCount / truncatedTotal, truncatedMin, truncatedMax, truncatedArithmetic: truncatedTotal / truncatedCount, truncatedGeometric: Math.exp(truncatedLogarithms / truncatedCount), truncatedHarmonic: truncatedCount / truncatedReciprocals }; } // src/bin/cli.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later process.title = "NanoPow CLI"; var logger = new Logger(); delete process.env.NANO_POW_DEBUG; delete process.env.NANO_POW_EFFORT; delete process.env.NANO_POW_PORT; var hashes = []; var stdinErrors = []; if (!process.stdin.isTTY) { const stdin = createInterface({ input: process.stdin }); let i = 0; for await (const line of stdin) { i++; if (isHex32(line)) { hashes.push(line); } else { stdinErrors.push(`Skipping invalid stdin input line ${i}`); } } } var args = process.argv.slice(2); if (hashes.length === 0 && args.length === 0 || args.some((v) => v === "--help" || v === "-h")) { console.log(`Usage: nano-pow [OPTION]... BLOCKHASH... Generate work for BLOCKHASH, or multiple work values for BLOCKHASH(es) BLOCKHASH is a 64-character hexadecimal string. Multiple blockhashes must be separated by whitespace or line breaks. Prints the result as a Javascript object to standard output as soon as it is calculated. If using --batch, results are printed only after all BLOCKHASH(es) have be processed. If using --validate, results will also include validity properties. -b, --batch process all data before returning final results as array -d, --difficulty <value> override the minimum difficulty value -e, --effort <value> increase demand on GPU processing -v, --validate <value> check an existing work value instead of searching for one -h, --help show this dialog --debug enable additional logging output --benchmark <value> generate work for specified number of random hashes --score <value> used with --benchmark to run specified number of times and calculate work-per-second If validating a nonce, it must be a 16-character hexadecimal value. Effort must be a decimal number between 1-32. Difficulty must be a hexadecimal string between 0-FFFFFFFFFFFFFFFF. Report bugs: <bug-nano-pow@zoso.dev> Full documentation: <https://www.npmjs.com/package/nano-pow> `); process.exit(0); } var inArgs = []; var isParsingHash = true; while (isParsingHash) { if (!isHex8(args[args.length - 1])) break; try { inArgs.unshift(args.pop()); } catch { isParsingHash = false; } } hashes.push(...inArgs); var isBatch = false; var isBenchmark = false; var runs = 1; var body = { action: "work_generate" }; for (let i = 0; i < args.length; i++) { switch (args[i]) { case "--batch": case "-b": { isBatch = true; break; } case "--benchmark": { const b = args[i + 1]; if (b == null) throw new Error("Missing argument for benchmark"); const count = +b; if (count < 1) throw new Error("Invalid benchmark count"); generateHashes(count); isBenchmark = true; break; } case "--debug": { logger.isEnabled = true; process.env.NANO_POW_DEBUG = "true"; break; } case "--difficulty": case "-d": { const d = args[i + 1]; if (d == null) throw new Error("Missing argument for difficulty"); if (!isHex8(d)) throw new Error("Invalid argument for difficulty"); body.difficulty = d; break; } case "--effort": case "-e": { const e = args[i + 1]; if (e == null) throw new Error("Missing argument for effort"); if (parseInt(e) < 1 || parseInt(e) > 32) throw new Error("Invalid effort"); process.env.NANO_POW_EFFORT = e; break; } case "--score": { const s = args[i + 1]; if (s == null) throw new Error("Missing argument for score"); const count = +s; if (count < 1) throw new Error("Invalid score runs count"); runs = count; break; } case "--validate": case "-v": { const v = args[i + 1]; if (v == null) throw new Error("Missing argument for work validation"); if (!isHex8(v)) throw new Error("Invalid argument for work validation"); if (hashes.length !== 1) throw new Error("Validate accepts exactly one hash"); body.action = "work_validate"; body.work = v; break; } } } if (hashes.length === 0) { console.error("Invalid block hash input"); process.exit(1); } logger.log("CLI args: ", ...args); for (const stdinErr of stdinErrors) { logger.log(stdinErr); } logger.log("launching server"); var server = spawn( process.execPath, [new URL(import.meta.resolve("./server.js")).pathname], { stdio: ["pipe", "pipe", "pipe", "ipc"] } ); server.once("error", (err) => { logger.log(err); process.exit(1); }); server.on("message", async (msg) => { if (typeof msg === "object" && msg != null && "type" in msg && typeof msg.type === "string" && "data" in msg && typeof msg.data === "string") { if (msg.type === "console") { logger.log(msg.data); } if (msg.type === "listening") { if (msg.data === "ipc") { logger.log(`CLI connected to server over IPC`); try { if (isBenchmark) { if (runs > 1) { await score(); } else { await benchmark(); } } else { await execute(); } } catch { logger.log(`Error executing ${body.action}`); } finally { server.kill(); } } else { logger.log("CLI server failed to connect over IPC"); } } } else { logger.log("received invalid message from server", msg); } }); server.on("close", (code) => { logger.log(`Server closed with exit code ${code}`); process.exit(code); }); async function benchmark() { console.log("Running benchmark..."); let start = 0, end = 0; const times = []; for (const hash of hashes) { const kill = setTimeout(() => { throw new Error("cli execution timed out"); }, 6e4); body.hash = hash; try { start = performance.now(); await request(body); end = performance.now(); times.push(end - start); } catch (err) { logger.log(err); } finally { clearTimeout(kill); } } console.log(stats(times)); return stats(times); } async function execute() { const results = []; for (const hash of hashes) { const kill = setTimeout(() => { throw new Error("cli execution timed out"); }, 6e4); body.hash = hash; try { const result = await request(body); isBatch ? results.push(result) : console.log(result); } catch (err) { logger.log(err); } finally { clearTimeout(kill); } } if (isBatch) console.log(results); } function generateHashes(count) { const random = new Uint8Array(32); while (hashes.length < count) { getRandomValues(random); hashes.push(Buffer.from(random).toString("hex")); } } async function request(body2) { return new Promise((resolve, reject) => { const listener = async (msg) => { if (typeof msg === "object" && msg != null && "type" in msg && typeof msg.type === "string" && "data" in msg && typeof msg.data === "string") { if (msg.type === "ipc") { resolve(JSON.parse(msg.data)); server.off("message", listener); } } }; server.on("message", listener); server.send({ type: "ipc", data: JSON.stringify(body2) }, (cb) => cb ? reject(cb) : logger.log("cli ipc request to server")); }); } async function score() { console.log("Calculating work per second..."); const rates = []; for (let i = 0; i < runs; i++) { try { const result = await benchmark(); logger.log(result); if (result != null) rates.push(result.truncatedRate); const count = hashes.length; hashes.splice(0, hashes.length); generateHashes(count); } catch (err) { logger.log(err); } } console.log(stats(rates)?.truncatedHarmonic, "wps"); }