nano-pow
Version:
Proof-of-work generation and validation with WebGPU/WebGL for Nano cryptocurrency.
252 lines (245 loc) • 7.89 kB
JavaScript
// 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();
}