UNPKG

nano-pow

Version:

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

252 lines (245 loc) 7.89 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/index.ts function average(times) { if (times == null || times.length === 0) return {}; let count = times.length; let min = Number.MAX_SAFE_INTEGER; let logarithms, max, median, rate, reciprocals, total, truncated, truncatedCount; logarithms = max = median = rate = reciprocals = total = truncated = truncatedCount = 0; times.sort((a, b) => a - b); for (let i = 0; i < count; i++) { const time = times[i]; total += time; reciprocals += 1 / time; logarithms += Math.log(time); min = Math.min(min, time); max = Math.max(max, time); if (i + 1 === Math.ceil(count / 2)) median = time; if (count < 3 || i > 0.1 * count && i < 0.9 * (count - 1)) { truncated += time; truncatedCount++; } } return { count, total, min, max, median, arithmetic: total / count, harmonic: count / reciprocals, geometric: Math.exp(logarithms / count), truncated: truncated / truncatedCount, rate: 1e3 * truncatedCount / (truncated || total) }; } function isHex(input, min, max) { if (typeof input !== "string") { return false; } if (typeof min !== "undefined" && typeof min !== "number") { throw new Error(`Invalid argument for parameter 'min'`); } if (typeof max !== "undefined" && typeof max !== "number") { throw new Error(`Invalid argument for parameter 'max'`); } const range = min === void 0 && max === void 0 ? "+" : `{${min ?? "0"},${max ?? ""}}`; const regexp = new RegExp(`^[0-9A-Fa-f]${range}$`, "m"); return regexp.test(input); } function isNotHex(input, min, max) { return !isHex(input, min, max); } function log(...args2) { if (process?.env?.NANO_POW_DEBUG) { const entry = `${new Date(Date.now()).toLocaleString(Intl.DateTimeFormat().resolvedOptions().locale ?? "en-US", { hour12: false, dateStyle: "medium", timeStyle: "medium" })} NanoPow[${process.pid}]: ${args2}`; console.log(entry); process.send?.({ type: "console", message: entry }); } } // src/bin/cli.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later process.title = "NanoPow CLI"; 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 (isHex(line, 64)) { 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 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 = []; while (isHex(args[args.length - 1], 64)) { inArgs.unshift(args.pop()); } hashes.push(...inArgs); var isBatch = false; var isBenchmark = false; var body = { action: "work_generate" }; for (let i = 0; i < args.length; i++) { switch (args[i]) { case "--validate": case "-v": { const v = args[i + 1]; if (v == null) throw new Error("Missing argument for work validation"); if (isNotHex(v, 16)) throw new Error("Invalid work to validate"); if (hashes.length !== 1) throw new Error("Validate accepts exactly one hash"); body.action = "work_validate"; body.work = v; break; } case "--difficulty": case "-d": { const d = args[i + 1]; if (d == null) throw new Error("Missing argument for difficulty"); if (isNotHex(d, 1, 16)) throw new Error("Invalid 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 "--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"); const random = new Uint8Array(32); while (hashes.length < count) { getRandomValues(random); hashes.push(Buffer.from(random).toString("hex")); } isBenchmark = true; break; } case "--debug": { process.env.NANO_POW_DEBUG = "true"; break; } case "--batch": case "-b": { isBatch = true; break; } } } if (hashes.length === 0) { console.error("Invalid block hash input"); process.exit(1); } log("CLI args:", ...args); for (const stdinErr of stdinErrors) { log(stdinErr); } log("Starting NanoPow CLI"); var server = spawn( process.execPath, [new URL(import.meta.resolve("./server.js")).pathname], { stdio: ["pipe", "pipe", "pipe", "ipc"] } ); server.once("error", (err) => { log(err); process.exit(1); }); server.on("message", async (msg) => { if (msg.type === "console") { log(msg.message); } if (msg.type === "listening") { const port = +msg.message; if (port > -1) { log(`CLI server listening on port ${port}`); try { await execute(port); } catch { log(`Error executing ${body.action}`); } } else { log("Server failed to provide port"); } } }); server.on("close", (code) => { log(`Server closed with exit code ${code}`); process.exit(code); }); async function execute(port) { const results = []; if (isBenchmark) console.log("Running benchmark..."); let start = 0; const times = []; for (const hash of hashes) { try { const aborter = new AbortController(); const kill = setTimeout(() => aborter.abort(), 6e4); body.hash = hash; start = performance.now(); const response = await fetch(`http://localhost:${port}`, { method: "POST", body: JSON.stringify(body), signal: aborter.signal }); clearTimeout(kill); const result = await response.json(); if (isBatch || isBenchmark) { results.push(result); times.push(performance.now() - start); } else { console.log(result); } } catch (err) { log(err); } } if (isBatch && !isBenchmark) console.log(results); if (process.env.NANO_POW_DEBUG || isBenchmark) console.log(average(times)); server.kill(); }