UNPKG

nano-pow

Version:

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

264 lines (259 loc) 8.19 kB
#!/usr/bin/env node //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com> //! SPDX-License-Identifier: GPL-3.0-or-later import { Logger, isHex32, isHex8, stats } from "nano-pow/utils"; import { spawn } from "node:child_process"; import { getRandomValues } from "node:crypto"; import { createInterface } from "node:readline/promises"; process.title = "NanoPow CLI"; const logger = new Logger(); delete process.env.NANO_POW_DEBUG; delete process.env.NANO_POW_EFFORT; delete process.env.NANO_POW_PORT; const hashes = []; const 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}`); } } } const 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@codecow.com> Full documentation: <https://www.npmjs.com/package/nano-pow> `); process.exit(0); } const inArgs = []; let isParsingHash = true; while (isParsingHash) { if (!isHex32(args[args.length - 1])) break; try { inArgs.unshift(args.pop()); } catch { isParsingHash = false; } } hashes.push(...inArgs); let isBatch = false; let isBenchmark = false; let runs = 1; const 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"); const 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"); } //# sourceMappingURL=cli.js.map