nano-pow
Version:
Proof-of-work generation and validation with WebGPU/WebGL/WASM for Nano cryptocurrency.
535 lines (519 loc) • 16.5 kB
JavaScript
// src/bin/server.ts
import * as http from "node:http";
import { hash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { launch } from "puppeteer";
// 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(...args) {
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 < args.length; i++) {
if (typeof args[i] === "string") {
args[i] = args[i].replace(datetime, "").trimStart();
}
if (args[i] instanceof Error) {
if ("stack" in args[i]) {
args.splice(i + 1, 0, args[i].stack);
} else if ("message" in args[i]) {
args.splice(i + 1, 0, args[i].message);
}
}
}
const entry = `${datetime} ${globalThis.process?.title ?? "NanoPow"}[${globalThis.process?.pid ?? "browser"}]:`;
console.log(entry, ...args);
globalThis.process?.send?.({ type: "console", data: `${entry} ${args}` });
}
}
};
// 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);
}
// src/bin/server.ts
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
//! SPDX-License-Identifier: GPL-3.0-or-later
process.title = "NanoPow Server";
await writeFile(`${homedir()}/.nano-pow/server.pid`, `${process.pid}
`);
var logger = new Logger();
var MAX_CONNECTIONS = 1024;
var MAX_HEADER_COUNT = 32;
var MAX_IDLE_TIME = 3e4;
var MAX_REQUEST_COUNT = 10;
var MAX_REQUEST_SIZE = 256;
var MAX_REQUEST_TIME = 6e4;
var CONFIG = {
DEBUG: false,
EFFORT: 4,
PORT: 5040
};
var configPatterns = {
DEBUG: {
r: /^[ \t]*debug[ \t]*(true|false)[ \t]*(#.*)?$/i,
v: (b) => ["1", "true", "yes"].includes(b)
},
EFFORT: {
r: /^[ \t]*effort[ \t]*(\d{1,2})[ \t]*(#.*)?$/i,
v: (n) => +n
},
PORT: {
r: /^[ \t]*port[ \t]*(\d{1,5})[ \t]*(#.*)?$/i,
v: (n) => +n
}
};
async function loadConfig() {
let configFile = "";
try {
configFile = await readFile(join(homedir(), ".nano-pow", "config"), "utf-8");
} catch {
}
if (configFile.length > 0) {
for (const line of configFile.split("\n")) {
for (const [k, { r, v }] of Object.entries(configPatterns)) {
const m = r.exec(line);
if (m) CONFIG[k] = v(m[1]);
}
}
}
CONFIG.DEBUG = ["1", "true", "yes"].includes(process.env.NANO_POW_DEBUG ?? "") || CONFIG.DEBUG;
CONFIG.EFFORT = +(process.env.NANO_POW_EFFORT ?? "") || CONFIG.EFFORT;
CONFIG.PORT = process.send ? 0 : +(process.env.NANO_POW_PORT ?? "") || CONFIG.PORT;
logger.isEnabled = CONFIG.DEBUG;
if (configFile.length === 0) LOG: logger.log("config file not found");
LOG: logger.log(`config set: ${JSON.stringify(CONFIG)}`);
}
await loadConfig();
LOG: logger.log("starting work server");
var NanoPow = await readFile(new URL("../main.min.js", import.meta.url), "utf-8");
LOG: logger.log("launching puppeteer browser");
var browser = await launch({
handleSIGHUP: false,
handleSIGINT: false,
handleSIGTERM: false,
headless: true,
args: [
"--disable-vulkan-surface",
"--enable-features=Vulkan,DefaultANGLEVulkan,VulkanFromANGLE",
"--enable-gpu",
"--enable-unsafe-webgpu"
]
});
LOG: logger.log("creating puppeteer page");
var page = await browser.newPage();
var src = `${NanoPow};window.NanoPow=NanoPow;`;
var enc = `sha256-${hash("sha256", src, "base64")}`;
var body = `
<!DOCTYPE html>
<link rel="icon" href="data:,">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'; script-src '${enc}' 'wasm-unsafe-eval'; worker-src blob: https://nanopow.invalid;">
<script type="module">${src}</script>
`;
LOG: logger.log("setting puppeteer request interception");
await page.setRequestInterception(true);
page.on("request", (req) => {
if (req.isInterceptResolutionHandled()) return;
if (req.url() === "https://nanopow.invalid/") {
req.respond({ status: 200, contentType: "text/html", body });
} else {
req.continue();
}
});
page.on("console", (msg) => logger.log(msg.text()));
LOG: logger.log("navigating to https://nanopow.invalid/ to be intercepted");
await page.goto("https://nanopow.invalid/");
var NanoPowHandle = await page.waitForFunction(async () => {
await window.navigator.gpu.requestAdapter();
return window.NanoPow;
});
LOG: logger.log("puppeteer initialized");
var requests = /* @__PURE__ */ new Map();
setInterval(() => {
const now = Date.now();
for (const [ip, { time }] of requests) {
if (time < now - MAX_REQUEST_TIME) {
requests.delete(ip);
}
}
}, MAX_REQUEST_TIME);
async function work(data) {
let resBody = "request failed";
if (data == null || typeof data !== "object") {
LOG: logger.log("Failed to parse request");
throw new Error(resBody);
}
if (Object.getPrototypeOf(data) !== Object.prototype) {
LOG: logger.log("Data corrupted.");
throw new Error(resBody);
}
const dataParsed = data;
let { action, hash: hash2, work: work2, difficulty } = dataParsed;
if (action !== "work_generate" && action !== "work_validate") {
LOG: logger.log("Action must be work_generate or work_validate.");
throw new Error(resBody);
}
resBody = `${action} failed`;
if (!isHex32(hash2)) {
LOG: logger.log("Hash must be hex char(64)");
throw new Error(resBody);
}
if (difficulty !== void 0 && !isHex8(difficulty)) {
LOG: logger.log("Difficulty must be hex char(16).");
throw new Error(resBody);
}
if (action === "work_validate") {
if (!isHex8(work2)) {
LOG: logger.log("Work must be hex char(16).");
throw new Error(resBody);
}
} else {
let f2 = function(n) {
if (false) throw new Error();
};
var f = f2;
f2(work2);
}
const options = {
api: "webgpu",
debug: CONFIG.DEBUG,
effort: CONFIG.EFFORT,
difficulty
};
try {
const result = action === "work_validate" ? await page.evaluate((n, w, h, o) => {
if (n == null) throw new Error("NanoPow not found");
return n.work_validate(w, h, o);
}, NanoPowHandle, work2, hash2, options) : await page.evaluate((n, h, o) => {
if (n == null) throw new Error("NanoPow not found");
return n.work_generate(h, o);
}, NanoPowHandle, hash2, options);
resBody = JSON.stringify(result);
return resBody;
} catch (err) {
LOG: logger.log(err);
throw new Error(resBody);
}
}
function get(res) {
res.writeHead(200, { "Content-Type": "text/plain" }).end(`Usage: Send POST request to server URL to generate or validate Nano proof-of-work
Generate work for a HASH with an optional DIFFICULTY:
curl -d '{ action: "work_generate", hash: HASH, difficulty?: DIFFICULTY }'
Validate WORK previously calculated for a HASH with an optional DIFFICULTY:
curl -d '{ action: "work_validate", work: WORK, hash: HASH, difficulty?: DIFFICULTY }'
HASH is a big-endian 64-character hexadecimal string.
WORK is little-endian 16-character hexadecimal string.
DIFFICULTY is an optional 16-character hexadecimal string (default: FFFFFFF800000000)
Report bugs: <bug-nano-pow@zoso.dev>
Full documentation: <https://www.npmjs.com/package/nano-pow>
`);
}
async function post(res, reqData) {
const resHeaders = { "Content-Type": "application/json" };
let resStatusCode = 500;
let resBody = "request failed";
try {
resBody = await work(JSON.parse(Buffer.concat(reqData).toString()));
resStatusCode = 200;
} catch (err) {
resBody = err.message;
resStatusCode = 400;
} finally {
res.writeHead(resStatusCode, resHeaders).end(resBody);
}
}
function getIp(req) {
const xff = req.headers["x-forwarded-for"];
const ip = typeof xff === "string" ? xff.split(",")[0].trim().replace(/^::ffff:/, "") : req.socket.remoteAddress;
return ip;
}
function isRateLimited(ip) {
if (ip === "127.0.0.1" || ip === "::1" || process.send) {
return false;
}
const client = requests.get(ip);
if (client && client.tokens-- <= 0) {
LOG: logger.log(`==== Potential Abuse: ${ip} ====`);
return true;
}
if (Date.now() - MAX_REQUEST_TIME > (client?.time ?? 0)) {
requests.set(ip, { tokens: MAX_REQUEST_COUNT, time: Date.now() });
}
return false;
}
function listen() {
if (process.channel) {
process.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 === "ipc") {
const data = JSON.parse(msg.data);
try {
const result = await work(data);
process.send?.({ type: "ipc", data: result });
} catch (err) {
process.send?.({ type: "ipc", data: err.stack });
}
}
}
});
LOG: logger.log(`server listening on ipc`);
process.send?.({ type: "listening", data: "ipc" });
} else {
server.listen(CONFIG.PORT, "127.0.0.1", () => {
const address = server.address();
if (address == null) {
LOG: logger.log(`server closed`);
} else if (typeof address === "string") {
LOG: logger.log(`server listening on ${address}`);
process.send?.({ type: "listening", data: address });
} else {
CONFIG.PORT = address.port;
LOG: logger.log(`server listening on port ${address.port}`);
process.send?.({ type: "listening", data: address.port });
}
});
}
}
async function readIncomingMessage(req) {
return new Promise((resolve, reject) => {
const contentLength = +(req.headers["content-length"] ?? 0);
if (contentLength === 0 || contentLength > MAX_REQUEST_SIZE) {
reject(new Error("Content Too Large", { cause: { code: 413 } }));
}
const kill = setTimeout(() => {
reject(new Error("Request Timeout", { cause: { code: 408 } }));
}, MAX_IDLE_TIME);
const reqData = [];
let reqSize = 0;
req.on("data", (chunk) => {
reqSize += chunk.byteLength;
if (reqSize > MAX_REQUEST_SIZE) {
reject(new Error("Content Too Large", { cause: { code: 413 } }));
}
reqData.push(chunk);
});
req.on("end", () => {
clearTimeout(kill);
resolve(reqData);
});
req.once("error", reject);
});
}
var server = http.createServer(async (req, res) => {
const ip = getIp(req);
if (!ip) return res.writeHead(401).end("Unauthorized");
if (isRateLimited(ip)) return res.writeHead(429).end("Too Many Requests");
if (req.method === "POST") {
try {
const reqData = await readIncomingMessage(req);
post(res, reqData);
} catch (err) {
req.socket.destroy();
return res.writeHead(err.cause?.code ?? 500).end(err.message ?? "Internal Server Error");
}
} else {
get(res);
}
});
server.headersTimeout = MAX_IDLE_TIME;
server.keepAliveTimeout = MAX_IDLE_TIME;
server.maxConnections = MAX_CONNECTIONS;
server.maxHeadersCount = MAX_HEADER_COUNT;
server.on("connection", (s) => {
s.setTimeout(MAX_IDLE_TIME, () => s.destroy());
});
server.on("error", (serverErr) => {
LOG: logger.log("server error", serverErr);
try {
shutdown();
} catch (shutdownErr) {
LOG: logger.log("server failed to shut down", shutdownErr);
process.exit(1);
}
});
function shutdown() {
LOG: logger.log("shutdown signal received");
const kill = setTimeout(() => {
LOG: logger.log("server unresponsive, forcefully stopped");
process.exit(1);
}, 1e4);
server.close(async () => {
await browser.close();
clearTimeout(kill);
LOG: logger.log("server stopped");
process.exit(0);
});
}
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
process.on("SIGHUP", async () => {
LOG: logger.log("reloading configuration");
server.close(async () => {
await loadConfig();
await page.reload();
NanoPowHandle = await page.waitForFunction(() => {
return window.NanoPow;
});
listen();
});
});
listen();