nano-pow 4.1.3 → 4.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/cli.js CHANGED
@@ -124,6 +124,9 @@ log("Starting NanoPow CLI");
124
124
  const server = spawn(process.execPath, [new URL(import.meta.resolve("./server.js")).pathname], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
125
125
  const port = await new Promise((resolve, reject) => {
126
126
  server.on("message", (msg) => {
127
+ if (msg.type === "console") {
128
+ log(msg.text);
129
+ }
127
130
  if (msg.type === "listening") {
128
131
  if (msg.port != null) {
129
132
  log(`Server listening on port ${msg.port}`);
@@ -10,7 +10,7 @@ NANO_POW_LOGS="$NANO_POW_HOME"/logs;
10
10
  mkdir -p "$NANO_POW_LOGS";
11
11
  if [ "$1" = '--server' ]; then
12
12
  shift;
13
- node "$SCRIPT_DIR"/server.js > "$NANO_POW_LOGS"/nano-pow-server-$(date -u -Iseconds).log 2>&1 & echo "$!" > "$NANO_POW_HOME"/server.pid;
13
+ node "$SCRIPT_DIR"/server.js --max-http-header-size=1024 >> "$NANO_POW_LOGS"/nano-pow-server-$(date -I).log 2>&1 & echo "$!" > "$NANO_POW_HOME"/server.pid;
14
14
  sleep 0.1;
15
15
  if [ "$(ps | grep $(cat $NANO_POW_HOME/server.pid))" = '' ]; then
16
16
  cat $(ls -td "$NANO_POW_LOGS"/* | head -n1);
@@ -3,16 +3,33 @@
3
3
  //! SPDX-License-Identifier: GPL-3.0-or-later
4
4
  import { launch } from "puppeteer";
5
5
  import { subtle } from "node:crypto";
6
- import { readFile, unlink, writeFile } from "node:fs/promises";
6
+ import { readFile } from "node:fs/promises";
7
7
  import * as http from "node:http";
8
8
  import { homedir } from "node:os";
9
9
  import { join } from "node:path";
10
10
  function log(...args) {
11
- if (CONFIG.DEBUG) console.log(new Date(Date.now()).toLocaleString(), "NanoPow", args);
11
+ if (CONFIG.DEBUG) {
12
+ const text = `${new Date(Date.now()).toLocaleString(Intl.DateTimeFormat().resolvedOptions().locale ?? "en-US", { hour12: false, dateStyle: "medium", timeStyle: "medium" })} NanoPow[${process.pid}]: ${args}`;
13
+ console.log(text);
14
+ process.send?.({ type: "console", text });
15
+ }
12
16
  }
13
17
  process.title = "NanoPow Server";
18
+ const MAX_BODY_SIZE = 256;
19
+ const MAX_CONNECTIONS = 1024;
20
+ const MAX_HEADER_COUNT = 32;
21
+ const MAX_IDLE_TIME = 5e3;
22
+ const MAX_REQUEST_COUNT = 10;
14
23
  const MAX_REQUEST_SIZE = 1024;
15
- const MAX_BODY_SIZE = 158;
24
+ const MAX_REQUEST_TIME = 6e4;
25
+ const requests = /* @__PURE__ */ new Map();
26
+ setInterval(() => {
27
+ for (const [i, t] of requests) {
28
+ if (t.time < Date.now() - MAX_REQUEST_TIME) {
29
+ requests.delete(i);
30
+ }
31
+ }
32
+ }, Math.max(MAX_REQUEST_TIME, 0));
16
33
  const CONFIG = {
17
34
  DEBUG: false,
18
35
  EFFORT: 8,
@@ -50,42 +67,57 @@ process.on("SIGHUP", async () => {
50
67
  log("Reloading configuration");
51
68
  await loadConfig();
52
69
  });
53
- async function respond(res, data) {
70
+ async function respond(res, dataBuffer) {
54
71
  let statusCode = 500;
55
72
  let headers = { "Content-Type": "application/json" };
56
- let response = "work_validate failed";
73
+ let response = "request failed";
57
74
  try {
58
- const datastring = Buffer.concat(data).toString().replace(/\s+/g, "");
75
+ const datastring = Buffer.concat(dataBuffer).toString();
59
76
  if (Buffer.byteLength(datastring) > MAX_BODY_SIZE) {
60
- throw new Error("Invalid data.");
77
+ throw new Error("Data too large.");
78
+ }
79
+ const data = JSON.parse(datastring);
80
+ if (Object.getPrototypeOf(data) !== Object.prototype) {
81
+ throw new Error("Data corrupted.");
61
82
  }
62
- const { action, hash: hash2, work, difficulty } = JSON.parse(datastring);
83
+ const { action, hash: hash2, work, difficulty } = data;
63
84
  if (action !== "work_generate" && action !== "work_validate") {
64
- throw new Error("Invalid action. Must be work_generate or work_validate.");
85
+ throw new Error("Action must be work_generate or work_validate.");
65
86
  }
87
+ response = `${action} failed`;
66
88
  if (!/^[0-9A-Fa-f]{64}$/.test(hash2 ?? "")) {
67
- throw new Error("Invalid hash. Must be a 64-character hex string.");
89
+ throw new Error("Hash must be a 64-character hex string.");
68
90
  }
69
- if (difficulty && !/^[1-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(difficulty)) {
70
- throw new Error("Invalid difficulty. Must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF.");
91
+ if (difficulty && !/^[0-9A-Fa-f]{0,16}$/.test(difficulty)) {
92
+ throw new Error("Difficulty must be a hex string between 0-FFFFFFFFFFFFFFFF.");
71
93
  }
72
94
  if (action === "work_validate" && !/^[0-9A-Fa-f]{16}$/.test(work ?? "")) {
73
- throw new Error("Invalid work. Must be a 16-character hex string.");
95
+ throw new Error("Work must be a 16-character hex string.");
74
96
  }
75
- response = `${action} failed`;
76
97
  const options = {
77
98
  debug: CONFIG.DEBUG,
78
99
  effort: CONFIG.EFFORT,
79
100
  difficulty
80
101
  };
81
- const args = [];
82
- if (work) args.push(work);
83
- args.push(hash2);
84
- args.push(options);
85
- response = JSON.stringify(await page.evaluate(async (action2, args2) => {
86
- if (window.NanoPow == null) throw new Error("NanoPow not found");
87
- return await window.NanoPow[action2](...args2);
88
- }, action, args));
102
+ switch (action) {
103
+ case "work_generate": {
104
+ response = JSON.stringify(await page.evaluate(async (np, hash3, options2) => {
105
+ if (np == null) throw new Error("NanoPow not found");
106
+ return await np.work_generate(hash3, options2);
107
+ }, npHandle, hash2, options));
108
+ break;
109
+ }
110
+ case "work_validate": {
111
+ response = JSON.stringify(await page.evaluate(async (np, work2, hash3, options2) => {
112
+ if (np == null) throw new Error("NanoPow not found");
113
+ return await np.work_validate(work2, hash3, options2);
114
+ }, npHandle, work, hash2, options));
115
+ break;
116
+ }
117
+ default: {
118
+ throw new Error("Action must be work_generate or work_validate.");
119
+ }
120
+ }
89
121
  statusCode = 200;
90
122
  } catch (err) {
91
123
  log(err);
@@ -95,14 +127,35 @@ async function respond(res, data) {
95
127
  }
96
128
  }
97
129
  const server = http.createServer((req, res) => {
130
+ const xff = req.headers["x-forwarded-for"];
131
+ const ip = typeof xff === "string" ? xff.split(",")[0].trim().replace(/^::ffff:/, "") : req.socket.remoteAddress;
132
+ if (ip == null) {
133
+ res.writeHead(401).end("Unauthorized");
134
+ return;
135
+ }
136
+ const client = requests.get(ip);
137
+ if (ip === "127.0.0.1" || process.send != null || client == null || client.time < Date.now() - MAX_REQUEST_TIME) {
138
+ requests.set(ip, { tokens: MAX_REQUEST_COUNT, time: Date.now() });
139
+ } else {
140
+ if (--client.tokens <= 0) {
141
+ log(`${ip} potential abuse`);
142
+ res.writeHead(429).end("Too Many Requests");
143
+ return;
144
+ }
145
+ }
98
146
  let data = [];
99
147
  let reqSize = 0;
100
148
  if (req.method === "POST") {
149
+ const contentLength = +(req.headers["content-length"] ?? 0);
150
+ if (contentLength == 0 || contentLength > MAX_BODY_SIZE) {
151
+ res.writeHead(413).end("Content Too Large");
152
+ req.socket.destroy();
153
+ return;
154
+ }
101
155
  req.on("data", (chunk) => {
102
156
  reqSize += chunk.byteLength;
103
157
  if (reqSize > MAX_REQUEST_SIZE) {
104
- res.writeHead(413, { "Content-Type": "text/plain" });
105
- res.end("Content Too Large");
158
+ res.writeHead(413).end("Content Too Large");
106
159
  req.socket.destroy();
107
160
  return;
108
161
  }
@@ -134,6 +187,13 @@ Full documentation: <https://www.npmjs.com/package/nano-pow>
134
187
  );
135
188
  }
136
189
  });
190
+ server.headersTimeout = MAX_IDLE_TIME;
191
+ server.keepAliveTimeout = MAX_IDLE_TIME;
192
+ server.maxConnections = MAX_CONNECTIONS;
193
+ server.maxHeadersCount = MAX_HEADER_COUNT;
194
+ server.on("connection", (c) => {
195
+ c.setTimeout(MAX_IDLE_TIME, () => c.destroy());
196
+ });
137
197
  server.on("error", (e) => {
138
198
  log("Server error", e);
139
199
  try {
@@ -167,35 +227,41 @@ const browser = await launch({
167
227
  headless: true,
168
228
  args: [
169
229
  "--headless=new",
170
- "--use-angle=vulkan",
171
- "--enable-features=Vulkan",
172
230
  "--disable-vulkan-surface",
231
+ "--enable-features=Vulkan,DefaultANGLEVulkan,VulkanFromANGLE",
232
+ "--enable-gpu",
173
233
  "--enable-unsafe-webgpu"
174
234
  ]
175
235
  });
176
236
  const page = await browser.newPage();
177
- page.on("console", (msg) => log(msg.text()));
178
- const path = new URL(import.meta.url).pathname;
179
- const dir = path.slice(0, path.lastIndexOf("/"));
180
- const filename = join(dir, `${process.pid}.html`);
181
- await writeFile(filename, '<!DOCTYPE html><link rel="icon" href="data:,">');
182
- await page.goto(import.meta.resolve(filename));
183
- await page.waitForFunction(async () => {
184
- return await navigator["gpu"].requestAdapter();
185
- });
186
- const src = `${NanoPow};window.NanoPow=NanoPow;`;
237
+ const src = `${NanoPow};window.NanoPow=NanoPowGpu;`;
187
238
  const hash = await subtle.digest("SHA-256", Buffer.from(src));
188
239
  const enc = `sha256-${Buffer.from(hash).toString("base64")}`;
189
- await page.setContent(`
240
+ const body = `
190
241
  <!DOCTYPE html>
191
242
  <link rel="icon" href="data:,">
192
243
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'; script-src '${enc}';">
193
244
  <script type="module">${src}</script>
194
- `);
195
- await unlink(filename);
245
+ `;
246
+ await page.setRequestInterception(true);
247
+ page.on("request", async (req) => {
248
+ if (req.isInterceptResolutionHandled()) return;
249
+ if (req.url() === "https://nanopow.invalid/") {
250
+ req.respond({ status: 200, contentType: "text/html", body });
251
+ } else {
252
+ req.continue();
253
+ }
254
+ });
255
+ page.on("console", (msg) => log(msg.text()));
256
+ await page.goto("https://nanopow.invalid/");
257
+ await page.waitForFunction(async () => {
258
+ return window.NanoPow != null;
259
+ });
260
+ const npHandle = await page.evaluateHandle(() => window.NanoPow);
196
261
  log("Puppeteer initialized");
197
- server.listen(CONFIG.PORT, async () => {
262
+ server.listen(CONFIG.PORT, "127.0.0.1", async () => {
198
263
  const { port } = server.address();
199
- log(`Server process ${process.pid} listening on port ${port}`);
264
+ CONFIG.PORT = port;
265
+ log(`Server listening on port ${port}`);
200
266
  process.send?.({ type: "listening", port });
201
267
  });
package/dist/main.min.js CHANGED
@@ -506,7 +506,7 @@ var NanoPowGl = class _NanoPowGl {
506
506
  // src/lib/gpu/compute.wgsl
507
507
  var compute_default = `//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
508
508
  //! SPDX-License-Identifier: GPL-3.0-or-later
509
- struct UBO{blockhash:array<vec4<u32>,2>,seed:vec2<u32>,difficulty:vec2<u32>};@group(0)@binding(0)var<uniform> ubo:UBO;struct WORK{found:atomic<u32>,nonce:vec2<u32>,result:vec2<u32>};@group(0)@binding(1)var<storage,read_write>work:WORK;const BLAKE2B_IV=array<vec2<u32>,8>(vec2<u32>(0xF3BCC908u,0x6A09E667u),vec2<u32>(0x84CAA73Bu,0xBB67AE85u),vec2<u32>(0xFE94F82Bu,0x3C6EF372u),vec2<u32>(0x5F1D36F1u,0xA54FF53Au),vec2<u32>(0xADE682D1u,0x510E527Fu),vec2<u32>(0x2B3E6C1Fu,0x9B05688Cu),vec2<u32>(0xFB41BD6Bu,0x1F83D9ABu),vec2<u32>(0x137E2179u,0x5BE0CD19u));const BLAKE2B_PARAM=vec2<u32>(0x01010008u,0u);const BLAKE2B_INLEN=vec2<u32>(0x00000028u,0u);const BLAKE2B_FINAL=vec2<u32>(0xFFFFFFFFu,0xFFFFFFFFu);const BLAKE2B_INIT=array<vec2<u32>,16>(BLAKE2B_IV[0u]^ BLAKE2B_PARAM,BLAKE2B_IV[1u],BLAKE2B_IV[2u],BLAKE2B_IV[3u],BLAKE2B_IV[4u],BLAKE2B_IV[5u],BLAKE2B_IV[6u],BLAKE2B_IV[7u],BLAKE2B_IV[0u],BLAKE2B_IV[1u],BLAKE2B_IV[2u],BLAKE2B_IV[3u],BLAKE2B_IV[4u]^ BLAKE2B_INLEN,BLAKE2B_IV[5u],BLAKE2B_IV[6u]^ BLAKE2B_FINAL,BLAKE2B_IV[7u]);fn G(a:ptr<function,vec2<u32>>,b:ptr<function,vec2<u32>>,c:ptr<function,vec2<u32>>,d:ptr<function,vec2<u32>>,m0:vec2<u32>,m1:vec2<u32>){*a+=*b;(*a).y+=u32((*a).x<(*b).x);*a+=m0;(*a).y+=u32((*a).x<m0.x);*d=(*d ^*a).yx;*c+=*d;(*c).y+=u32((*c).x<(*d).x);*b ^=*c;*b=(*b>>vec2(24u))|(*b<<vec2(8u)).yx;*a+=*b;(*a).y+=u32((*a).x<(*b).x);*a+=m1;(*a).y+=u32((*a).x<m1.x);*d ^=*a;*d=(*d>>vec2(16u))|(*d<<vec2(16u)).yx;*c+=*d;(*c).y+=u32((*c).x<(*d).x);*b ^=*c;*b=(*b>>vec2(31u)).yx|(*b<<vec2(1u));}const Z=vec2(0u);var<workgroup> found:bool;var<workgroup> seed:vec2<u32>;var<workgroup> m1:vec2<u32>;var<workgroup> m2:vec2<u32>;var<workgroup> m3:vec2<u32>;var<workgroup> m4:vec2<u32>;@compute @workgroup_size(96)fn search(@builtin(global_invocation_id)global_id:vec3<u32>,@builtin(local_invocation_id)local_id:vec3<u32>){if(local_id.x==0u){found=atomicLoad(&work.found)!=0u;seed=ubo.seed;m1=ubo.blockhash[0u].xy;m2=ubo.blockhash[0u].zw;m3=ubo.blockhash[1u].xy;m4=ubo.blockhash[1u].zw;}workgroupBarrier();if(found){return;}main(global_id,false);}@compute @workgroup_size(1)fn validate(@builtin(global_invocation_id)global_id:vec3<u32>){seed=ubo.seed;m1=ubo.blockhash[0u].xy;m2=ubo.blockhash[0u].zw;m3=ubo.blockhash[1u].xy;m4=ubo.blockhash[1u].zw;main(global_id,true);}fn main(id:vec3<u32>,validate:bool){let m0:vec2<u32>=seed ^ id.xy;var v0:vec2<u32>=vec2<u32>(BLAKE2B_INIT[0u]);var v1:vec2<u32>=vec2<u32>(BLAKE2B_INIT[1u]);var v2:vec2<u32>=vec2<u32>(BLAKE2B_INIT[2u]);var v3:vec2<u32>=vec2<u32>(BLAKE2B_INIT[3u]);var v4:vec2<u32>=vec2<u32>(BLAKE2B_INIT[4u]);var v5:vec2<u32>=vec2<u32>(BLAKE2B_INIT[5u]);var v6:vec2<u32>=vec2<u32>(BLAKE2B_INIT[6u]);var v7:vec2<u32>=vec2<u32>(BLAKE2B_INIT[7u]);var v8:vec2<u32>=vec2<u32>(BLAKE2B_INIT[8u]);var v9:vec2<u32>=vec2<u32>(BLAKE2B_INIT[9u]);var vA:vec2<u32>=vec2<u32>(BLAKE2B_INIT[10u]);var vB:vec2<u32>=vec2<u32>(BLAKE2B_INIT[11u]);var vC:vec2<u32>=vec2<u32>(BLAKE2B_INIT[12u]);var vD:vec2<u32>=vec2<u32>(BLAKE2B_INIT[13u]);var vE:vec2<u32>=vec2<u32>(BLAKE2B_INIT[14u]);var vF:vec2<u32>=vec2<u32>(BLAKE2B_INIT[15u]);G(&v0,&v4,&v8,&vC,m0,m1);G(&v1,&v5,&v9,&vD,m2,m3);G(&v2,&v6,&vA,&vE,m4,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,Z,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,m4,Z);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,m1,Z);G(&v1,&v6,&vB,&vC,m0,m2);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,Z,m3);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,Z,m0);G(&v2,&v6,&vA,&vE,Z,m2);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,Z,Z);G(&v1,&v6,&vB,&vC,m3,Z);G(&v2,&v7,&v8,&vD,Z,m1);G(&v3,&v4,&v9,&vE,Z,m4);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,m3,m1);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,m2,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,m4,m0);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,m0);G(&v1,&v5,&v9,&vD,Z,Z);G(&v2,&v6,&vA,&vE,m2,m4);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,Z,m1);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,m3,Z);G(&v0,&v4,&v8,&vC,m2,Z);G(&v1,&v5,&v9,&vD,Z,Z);G(&v2,&v6,&vA,&vE,m0,Z);G(&v3,&v7,&vB,&vF,Z,m3);G(&v0,&v5,&vA,&vF,m4,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,m1,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,m1,Z);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,m4,Z);G(&v0,&v5,&vA,&vF,m0,Z);G(&v1,&v6,&vB,&vC,Z,m3);G(&v2,&v7,&v8,&vD,Z,m2);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,Z,Z);G(&v2,&v6,&vA,&vE,Z,m1);G(&v3,&v7,&vB,&vF,m3,Z);G(&v0,&v5,&vA,&vF,Z,m0);G(&v1,&v6,&vB,&vC,Z,m4);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,m2,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,Z,Z);G(&v2,&v6,&vA,&vE,Z,m3);G(&v3,&v7,&vB,&vF,m0,Z);G(&v0,&v5,&vA,&vF,Z,m2);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,m1,m4);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,m2);G(&v1,&v5,&v9,&vD,Z,m4);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,m1,Z);G(&v0,&v5,&vA,&vF,Z,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,m3,Z);G(&v3,&v4,&v9,&vE,Z,m0);G(&v0,&v4,&v8,&vC,m0,m1);G(&v1,&v5,&v9,&vD,m2,m3);G(&v2,&v6,&vA,&vE,m4,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,Z,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,m4,Z);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,m1,Z);G(&v2,&v7,&v8,&vD,Z,Z);var result:vec2<u32>=BLAKE2B_INIT[0u]^ v0 ^ v8;if(validate||((result.y>ubo.difficulty.y||(result.y==ubo.difficulty.y&&result.y>=ubo.difficulty.y))&&atomicLoad(&work.found)==0u)){atomicStore(&work.found,1u);work.nonce=m0;work.result=result;}return;}`;
509
+ struct UBO{blockhash:array<vec4<u32>,2>,seed:vec2<u32>,difficulty:vec2<u32>};@group(0)@binding(0)var<uniform> ubo:UBO;struct WORK{found:atomic<u32>,nonce:vec2<u32>,result:vec2<u32>};@group(0)@binding(1)var<storage,read_write>work:WORK;const BLAKE2B_IV=array<vec2<u32>,8>(vec2<u32>(0xF3BCC908u,0x6A09E667u),vec2<u32>(0x84CAA73Bu,0xBB67AE85u),vec2<u32>(0xFE94F82Bu,0x3C6EF372u),vec2<u32>(0x5F1D36F1u,0xA54FF53Au),vec2<u32>(0xADE682D1u,0x510E527Fu),vec2<u32>(0x2B3E6C1Fu,0x9B05688Cu),vec2<u32>(0xFB41BD6Bu,0x1F83D9ABu),vec2<u32>(0x137E2179u,0x5BE0CD19u));const BLAKE2B_PARAM=vec2<u32>(0x01010008u,0u);const BLAKE2B_INLEN=vec2<u32>(0x00000028u,0u);const BLAKE2B_FINAL=vec2<u32>(0xFFFFFFFFu,0xFFFFFFFFu);const BLAKE2B_INIT=array<vec2<u32>,16>(BLAKE2B_IV[0u]^ BLAKE2B_PARAM,BLAKE2B_IV[1u],BLAKE2B_IV[2u],BLAKE2B_IV[3u],BLAKE2B_IV[4u],BLAKE2B_IV[5u],BLAKE2B_IV[6u],BLAKE2B_IV[7u],BLAKE2B_IV[0u],BLAKE2B_IV[1u],BLAKE2B_IV[2u],BLAKE2B_IV[3u],BLAKE2B_IV[4u]^ BLAKE2B_INLEN,BLAKE2B_IV[5u],BLAKE2B_IV[6u]^ BLAKE2B_FINAL,BLAKE2B_IV[7u]);fn G(a:ptr<function,vec2<u32>>,b:ptr<function,vec2<u32>>,c:ptr<function,vec2<u32>>,d:ptr<function,vec2<u32>>,m0:vec2<u32>,m1:vec2<u32>){*a+=*b;(*a).y+=u32((*a).x<(*b).x);*a+=m0;(*a).y+=u32((*a).x<m0.x);*d=(*d ^*a).yx;*c+=*d;(*c).y+=u32((*c).x<(*d).x);*b ^=*c;*b=(*b>>vec2(24u))|(*b<<vec2(8u)).yx;*a+=*b;(*a).y+=u32((*a).x<(*b).x);*a+=m1;(*a).y+=u32((*a).x<m1.x);*d ^=*a;*d=(*d>>vec2(16u))|(*d<<vec2(16u)).yx;*c+=*d;(*c).y+=u32((*c).x<(*d).x);*b ^=*c;*b=(*b>>vec2(31u)).yx|(*b<<vec2(1u));}const Z=vec2(0u);var<workgroup> found:bool;var<workgroup> seed:vec2<u32>;var<workgroup> m1:vec2<u32>;var<workgroup> m2:vec2<u32>;var<workgroup> m3:vec2<u32>;var<workgroup> m4:vec2<u32>;@compute @workgroup_size(64)fn search(@builtin(global_invocation_id)global_id:vec3<u32>,@builtin(local_invocation_id)local_id:vec3<u32>){if(local_id.x==0u){found=atomicLoad(&work.found)!=0u;seed=ubo.seed;m1=ubo.blockhash[0u].xy;m2=ubo.blockhash[0u].zw;m3=ubo.blockhash[1u].xy;m4=ubo.blockhash[1u].zw;}workgroupBarrier();if(found){return;}main(global_id,false);}@compute @workgroup_size(1)fn validate(@builtin(global_invocation_id)global_id:vec3<u32>){seed=ubo.seed;m1=ubo.blockhash[0u].xy;m2=ubo.blockhash[0u].zw;m3=ubo.blockhash[1u].xy;m4=ubo.blockhash[1u].zw;main(global_id,true);}fn main(id:vec3<u32>,validate:bool){let m0:vec2<u32>=seed ^ id.xy;var v0:vec2<u32>=vec2<u32>(BLAKE2B_INIT[0u]);var v1:vec2<u32>=vec2<u32>(BLAKE2B_INIT[1u]);var v2:vec2<u32>=vec2<u32>(BLAKE2B_INIT[2u]);var v3:vec2<u32>=vec2<u32>(BLAKE2B_INIT[3u]);var v4:vec2<u32>=vec2<u32>(BLAKE2B_INIT[4u]);var v5:vec2<u32>=vec2<u32>(BLAKE2B_INIT[5u]);var v6:vec2<u32>=vec2<u32>(BLAKE2B_INIT[6u]);var v7:vec2<u32>=vec2<u32>(BLAKE2B_INIT[7u]);var v8:vec2<u32>=vec2<u32>(BLAKE2B_INIT[8u]);var v9:vec2<u32>=vec2<u32>(BLAKE2B_INIT[9u]);var vA:vec2<u32>=vec2<u32>(BLAKE2B_INIT[10u]);var vB:vec2<u32>=vec2<u32>(BLAKE2B_INIT[11u]);var vC:vec2<u32>=vec2<u32>(BLAKE2B_INIT[12u]);var vD:vec2<u32>=vec2<u32>(BLAKE2B_INIT[13u]);var vE:vec2<u32>=vec2<u32>(BLAKE2B_INIT[14u]);var vF:vec2<u32>=vec2<u32>(BLAKE2B_INIT[15u]);G(&v0,&v4,&v8,&vC,m0,m1);G(&v1,&v5,&v9,&vD,m2,m3);G(&v2,&v6,&vA,&vE,m4,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,Z,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,m4,Z);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,m1,Z);G(&v1,&v6,&vB,&vC,m0,m2);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,Z,m3);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,Z,m0);G(&v2,&v6,&vA,&vE,Z,m2);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,Z,Z);G(&v1,&v6,&vB,&vC,m3,Z);G(&v2,&v7,&v8,&vD,Z,m1);G(&v3,&v4,&v9,&vE,Z,m4);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,m3,m1);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,m2,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,m4,m0);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,m0);G(&v1,&v5,&v9,&vD,Z,Z);G(&v2,&v6,&vA,&vE,m2,m4);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,Z,m1);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,m3,Z);G(&v0,&v4,&v8,&vC,m2,Z);G(&v1,&v5,&v9,&vD,Z,Z);G(&v2,&v6,&vA,&vE,m0,Z);G(&v3,&v7,&vB,&vF,Z,m3);G(&v0,&v5,&vA,&vF,m4,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,m1,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,m1,Z);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,m4,Z);G(&v0,&v5,&vA,&vF,m0,Z);G(&v1,&v6,&vB,&vC,Z,m3);G(&v2,&v7,&v8,&vD,Z,m2);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,Z,Z);G(&v2,&v6,&vA,&vE,Z,m1);G(&v3,&v7,&vB,&vF,m3,Z);G(&v0,&v5,&vA,&vF,Z,m0);G(&v1,&v6,&vB,&vC,Z,m4);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,m2,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,Z,Z);G(&v2,&v6,&vA,&vE,Z,m3);G(&v3,&v7,&vB,&vF,m0,Z);G(&v0,&v5,&vA,&vF,Z,m2);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,m1,m4);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,m2);G(&v1,&v5,&v9,&vD,Z,m4);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,m1,Z);G(&v0,&v5,&vA,&vF,Z,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,m3,Z);G(&v3,&v4,&v9,&vE,Z,m0);G(&v0,&v4,&v8,&vC,m0,m1);G(&v1,&v5,&v9,&vD,m2,m3);G(&v2,&v6,&vA,&vE,m4,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,Z,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,m4,Z);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,m1,Z);G(&v2,&v7,&v8,&vD,Z,Z);var result:vec2<u32>=BLAKE2B_INIT[0u]^ v0 ^ v8;if(validate||((result.y>ubo.difficulty.y||(result.y==ubo.difficulty.y&&result.x>=ubo.difficulty.x))&&atomicLoad(&work.found)==0u)){atomicStore(&work.found,1u);work.nonce=m0;work.result=result;}return;}`;
510
510
 
511
511
  // src/lib/gpu/index.ts
512
512
  //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
@@ -524,7 +524,9 @@ var NanoPowGpu = class _NanoPowGpu {
524
524
  static #cpuBuffer;
525
525
  static #uboBuffer;
526
526
  static #uboView;
527
+ static #resultView;
527
528
  static #bindGroupLayout;
529
+ static #bindGroup;
528
530
  static #searchPipeline;
529
531
  static #validatePipeline;
530
532
  // Initialize WebGPU
@@ -539,7 +541,7 @@ var NanoPowGpu = class _NanoPowGpu {
539
541
  if (!(device instanceof GPUDevice)) throw new Error("WebGPU device failed to load.");
540
542
  device.lost?.then(this.reset);
541
543
  this.#device = device;
542
- this.setup();
544
+ await this.setup();
543
545
  } catch (err) {
544
546
  throw new Error("WebGPU initialization failed.", { cause: err });
545
547
  } finally {
@@ -547,7 +549,7 @@ var NanoPowGpu = class _NanoPowGpu {
547
549
  }
548
550
  this.#isInitialized = true;
549
551
  }
550
- static setup() {
552
+ static async setup() {
551
553
  if (this.#device == null) throw new Error(`WebGPU device failed to load.`);
552
554
  this.#gpuBuffer = this.#device.createBuffer({
553
555
  size: 32,
@@ -588,6 +590,23 @@ var NanoPowGpu = class _NanoPowGpu {
588
590
  module: shaderModule
589
591
  }
590
592
  });
593
+ this.#bindGroup = this.#device.createBindGroup({
594
+ layout: this.#bindGroupLayout,
595
+ entries: [
596
+ {
597
+ binding: 0,
598
+ resource: {
599
+ buffer: this.#uboBuffer
600
+ }
601
+ },
602
+ {
603
+ binding: 1,
604
+ resource: {
605
+ buffer: this.#gpuBuffer
606
+ }
607
+ }
608
+ ]
609
+ });
591
610
  this.#validatePipeline = this.#device.createComputePipeline({
592
611
  layout: this.#device.createPipelineLayout({
593
612
  bindGroupLayouts: [this.#bindGroupLayout]
@@ -597,6 +616,10 @@ var NanoPowGpu = class _NanoPowGpu {
597
616
  module: shaderModule
598
617
  }
599
618
  });
619
+ const cmd = this.#device.createCommandEncoder();
620
+ cmd.beginComputePass().end();
621
+ this.#device.queue.submit([cmd.finish()]);
622
+ await this.#device.queue.onSubmittedWorkDone();
600
623
  console.log(`NanoPow WebGPU initialized. Recommended effort: ${Math.max(1, Math.floor(navigator.hardwareConcurrency / 2))}`);
601
624
  }
602
625
  static reset() {
@@ -652,45 +675,26 @@ var NanoPowGpu = class _NanoPowGpu {
652
675
  this.#device.queue.writeBuffer(this.#uboBuffer, 0, this.#uboView);
653
676
  this.#device.queue.writeBuffer(this.#gpuBuffer, 0, this.#bufferReset);
654
677
  this.#device.queue.writeBuffer(this.#cpuBuffer, 0, this.#bufferReset);
655
- const bindGroup = this.#device.createBindGroup({
656
- layout: this.#bindGroupLayout,
657
- entries: [
658
- {
659
- binding: 0,
660
- resource: {
661
- buffer: this.#uboBuffer
662
- }
663
- },
664
- {
665
- binding: 1,
666
- resource: {
667
- buffer: this.#gpuBuffer
668
- }
669
- }
670
- ]
671
- });
672
678
  const commandEncoder = this.#device.createCommandEncoder();
673
679
  const passEncoder = commandEncoder.beginComputePass();
674
680
  passEncoder.setPipeline(pipeline);
675
- passEncoder.setBindGroup(0, bindGroup);
681
+ passEncoder.setBindGroup(0, this.#bindGroup);
676
682
  passEncoder.dispatchWorkgroups(passes, passes);
677
683
  passEncoder.end();
678
684
  commandEncoder.copyBufferToBuffer(this.#gpuBuffer, 0, this.#cpuBuffer, 0, 32);
679
685
  this.#device.queue.submit([commandEncoder.finish()]);
680
- let data = null;
681
686
  try {
682
687
  await this.#cpuBuffer.mapAsync(GPUMapMode.READ);
683
688
  await this.#device.queue.onSubmittedWorkDone();
684
- data = new DataView(this.#cpuBuffer.getMappedRange().slice(0));
689
+ this.#resultView = new DataView(this.#cpuBuffer.getMappedRange().slice(0));
685
690
  this.#cpuBuffer.unmap();
686
691
  } catch (err) {
687
692
  console.warn(`Error getting data from GPU. ${err}`);
688
693
  this.#cpuBuffer.unmap();
689
694
  this.reset();
690
695
  }
691
- if (this.#debug) console.log("gpuBuffer data", data);
692
- if (data == null) throw new Error(`Failed to get data from buffer.`);
693
- return data;
696
+ if (this.#debug) console.log("gpuBuffer data", this.#resultView);
697
+ if (this.#resultView == null) throw new Error(`Failed to get data from buffer.`);
694
698
  }
695
699
  /**
696
700
  * Finds a nonce that satisfies the Nano proof-of-work requirements.
@@ -706,7 +710,7 @@ var NanoPowGpu = class _NanoPowGpu {
706
710
  setTimeout(async () => {
707
711
  const result2 = this.work_generate(hash, options);
708
712
  resolve(result2);
709
- }, 500);
713
+ }, 100);
710
714
  });
711
715
  }
712
716
  if (this.#isInitialized === false) this.init();
@@ -718,14 +722,14 @@ var NanoPowGpu = class _NanoPowGpu {
718
722
  throw new TypeError(`Invalid difficulty ${options.difficulty}`);
719
723
  }
720
724
  }
721
- const difficulty = typeof options?.difficulty !== "bigint" || options.difficulty < 1n || options.difficulty > 0xffffffffffffffffn ? 0xfffffff800000000n : options.difficulty;
725
+ const difficulty = typeof options?.difficulty !== "bigint" || options.difficulty < 0n || options.difficulty > 0xffffffffffffffffn ? 0xfffffff800000000n : options.difficulty;
722
726
  const effort = typeof options?.effort !== "number" || options.effort < 1 || options.effort > 32 ? 2048 : options.effort * 256;
723
727
  this.#debug = !!options?.debug;
724
728
  if (this.#debug) console.log("NanoPowGpu.work_generate()");
725
729
  if (this.#debug) console.log("blockhash", hash);
726
730
  if (this.#debug) console.log("search options", JSON.stringify(options, (k, v) => typeof v === "bigint" ? v.toString(16) : v));
727
731
  let loads = 0;
728
- while (this.#device == null && loads < 20) {
732
+ while (this.#device == null && loads++ < 20) {
729
733
  await new Promise((resolve) => {
730
734
  setTimeout(resolve, 500);
731
735
  });
@@ -738,16 +742,17 @@ var NanoPowGpu = class _NanoPowGpu {
738
742
  let start = performance.now();
739
743
  let nonce = 0n;
740
744
  let result = 0n;
745
+ let random = BigInt(Math.floor(Math.random() * 4294967295));
746
+ let seed = random;
741
747
  do {
742
748
  start = performance.now();
743
- const random0 = Math.floor(Math.random() * 4294967295);
744
- const random1 = Math.floor(Math.random() * 4294967295);
745
- const seed = BigInt(random0) << 32n | BigInt(random1);
749
+ random = BigInt(Math.floor(Math.random() * 4294967295));
750
+ seed = (seed & 0xffffffffn) << 32n | random;
746
751
  if (this.#debug) console.log("seed", seed.toString(16).padStart(16, "0"));
747
- const data = await this.#dispatch(this.#searchPipeline, seed, hash, difficulty, effort);
748
- const found = !!data.getUint32(0);
749
- nonce = data.getBigUint64(8, true);
750
- result = data.getBigUint64(16, true);
752
+ await this.#dispatch(this.#searchPipeline, seed, hash, difficulty, effort);
753
+ const found = !!this.#resultView.getUint32(0);
754
+ nonce = this.#resultView.getBigUint64(8, true);
755
+ result = this.#resultView.getBigUint64(16, true);
751
756
  this.#busy = !found;
752
757
  times.push(performance.now() - start);
753
758
  } while (this.#busy);
@@ -776,7 +781,7 @@ var NanoPowGpu = class _NanoPowGpu {
776
781
  setTimeout(async () => {
777
782
  const result2 = this.work_validate(work, hash, options);
778
783
  resolve(result2);
779
- }, 500);
784
+ }, 100);
780
785
  });
781
786
  }
782
787
  if (this.#isInitialized === false) this.init();
@@ -788,7 +793,7 @@ var NanoPowGpu = class _NanoPowGpu {
788
793
  throw new TypeError(`Invalid difficulty ${options.difficulty}`);
789
794
  }
790
795
  }
791
- const difficulty = typeof options?.difficulty !== "bigint" || options.difficulty < 1n || options.difficulty > 0xffffffffffffffffn ? 0xfffffff800000000n : options.difficulty;
796
+ const difficulty = typeof options?.difficulty !== "bigint" || options.difficulty < 0n || options.difficulty > 0xffffffffffffffffn ? 0xfffffff800000000n : options.difficulty;
792
797
  this.#debug = !!options?.debug;
793
798
  if (this.#debug) console.log("NanoPowGpu.work_validate()");
794
799
  if (this.#debug) console.log("blockhash", hash);
@@ -807,10 +812,11 @@ var NanoPowGpu = class _NanoPowGpu {
807
812
  let nonce = 0n;
808
813
  const seed = BigInt(`0x${work}`);
809
814
  if (this.#debug) console.log("work", work);
810
- const data = await this.#dispatch(this.#validatePipeline, seed, hash, difficulty, 1);
811
- nonce = data.getBigUint64(8, true);
812
- result = data.getBigUint64(16, true);
815
+ await this.#dispatch(this.#validatePipeline, seed, hash, difficulty, 1);
816
+ nonce = this.#resultView.getBigUint64(8, true);
817
+ result = this.#resultView.getBigUint64(16, true);
813
818
  this.#busy = false;
819
+ if (seed !== nonce) throw new Error("Result does not match work");
814
820
  if (this.#debug) console.log("nonce", nonce, nonce.toString(16).padStart(16, "0"));
815
821
  if (this.#debug) console.log("result", result, result.toString(16).padStart(16, "0"));
816
822
  const response = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-pow",
3
- "version": "4.1.3",
3
+ "version": "4.1.5",
4
4
  "description": "Proof-of-work generation and validation with WebGPU/WebGL for Nano cryptocurrency.",
5
5
  "keywords": [
6
6
  "nemo",
@@ -45,7 +45,7 @@
45
45
  "url": "git+https://zoso.dev/nano-pow.git"
46
46
  },
47
47
  "scripts": {
48
- "benchmark": "npm run build && ./dist/bin/nano-pow.sh --benchmark 1000",
48
+ "benchmark": "npm run build && ./dist/bin/nano-pow.sh --benchmark 100",
49
49
  "build": "rm -rf {dist,types} && tsc && node esbuild.mjs && cp -p src/bin/nano-pow.sh dist/bin",
50
50
  "prepare": "npm run build",
51
51
  "start": "./dist/bin/nano-pow.sh --server",