nano-pow
Version:
Proof-of-work generation and validation with WebGPU/WebGL/WASM for Nano cryptocurrency.
510 lines (494 loc) • 15.4 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/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");
}