nano-pow 4.0.6 → 4.0.8

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/README.md CHANGED
@@ -17,8 +17,10 @@ https://docs.nano.org/integration-guides/work-generation/#work-calculation-detai
17
17
 
18
18
  ## Installation
19
19
  ```console
20
- npm i nano-pow
20
+ $ npm i nano-pow
21
21
  ```
22
+ NanoPow can also be installed globally to add the `nano-pow` command to your
23
+ environment. To learn more, see [#Executables](#executables).
22
24
 
23
25
  ## Usage
24
26
  ### Import
@@ -46,17 +48,17 @@ Use it directly on a webpage with a script module:
46
48
  <script type="module">
47
49
  (async () => {
48
50
  const { NanoPow } = await import('https://cdn.jsdelivr.net/npm/nano-pow@latest')
49
- const work = await NanoPow.search(some_hash)
51
+ const { work } = await NanoPow.work_generate(some_hash)
50
52
  console.log(work)
51
53
  })()
52
54
  </script>
53
55
  ```
54
56
 
55
- ### Search
57
+ ### Generate
56
58
  ```javascript
57
59
  // `hash` is a 64-char hex string
58
60
  const hash = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
59
- const work = await NanoPow.search(hash)
61
+ const { work } = await NanoPow.work_generate(hash)
60
62
  // Result is a 16-char hex string
61
63
  ```
62
64
 
@@ -66,29 +68,112 @@ const work = await NanoPow.search(hash)
66
68
  const work = 'fedcba0987654321'
67
69
  // `hash` is a 64-char hex string
68
70
  const hash = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
69
- const isValid = await NanoPow.validate(work, hash)
71
+ const { valid } = await NanoPow.work_validate(work, hash)
70
72
  // Result is a boolean
71
73
  ```
72
74
 
73
75
  ### Options
74
76
  ```javascript
75
77
  const options = {
76
- // default 0xFFFFFFF8 for send/change blocks
78
+ // default 0xFFFFFFF800000000 for send/change blocks
77
79
  threshold: number,
78
80
  // default 8, valid range 1-32
79
81
  effort: number,
80
82
  // default false
81
83
  debug: true
82
84
  }
83
- const work = await NanoPow.search(hash, options)
85
+ const { work } = await NanoPow.work_generate(hash, options)
84
86
  ```
85
87
 
86
- ### Command Line
88
+ ## Executables
87
89
  NanoPow can be installed globally and executed from the command line. This is
88
90
  useful for systems without a graphical interface.
89
91
  ```console
90
92
  $ npm -g i nano-pow
91
- $ nano-pow --help # view command documentation
93
+ $ nano-pow --help # view abbreviated CLI help
94
+ $ man nano-pow # view full manual
95
+ ```
96
+ Ensure you have proper permissions on your
97
+ [npm `prefix`](https://docs.npmjs.com/cli/v11/commands/npm-prefix) directory and
98
+ have configured your `PATH` accordingly.
99
+
100
+ For example, this adds a user-specific directory for local binaries to `PATH`
101
+ upon login and configures `npm prefix` to use it for global installations:
102
+ ```console
103
+ $ echo 'PATH="$HOME/.local/bin:$PATH"' >> .bashrc
104
+ $ npm config set prefix="$HOME/.local"
105
+ ```
106
+ ### Command Line
107
+ NanoPow provides a shell command&mdash;`nano-pow`&mdash;to accomodate systems
108
+ without a graphical user interface. It launches a headless Chrome browser using
109
+ `puppeteer` to access the required WebGPU or WebGL APIs. Use the `--global` flag
110
+ when installing to add the executable script to your system.
111
+ ```console
112
+ $ npm i -g nano-pow
113
+ ```
114
+ Some examples are provided below, and for full documentation, read the manual
115
+ with `man nano-pow`.
116
+
117
+ ```console
118
+ $ # Generate a work value using default settings and debugging output enabled.
119
+ $ nano-pow --debug 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
120
+ ```
121
+ ```console
122
+ $ # Generate work using customized behavior with options.
123
+ $ nano-pow --effort 32 --threshold FFFFFFC000000000 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
124
+ ```
125
+ ```console
126
+ $ # Validate an existing work nonce against a blockhash.
127
+ nano-pow --validate fedcba9876543210 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
128
+ ```
129
+ ```console
130
+ $ # Process blockhashes in batches to reduce the initial startup overhead.
131
+ $ nano-pow 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef [...]
132
+ $ # OR
133
+ nano-pow $(cat /path/to/hashes/file)
134
+ $ # OR
135
+ $ cat /path/to/hashes/file | nano-pow
136
+ ```
137
+
138
+ ### Server
139
+ NanoPow also provides a basic work server similar to the one included in the
140
+ official Nano node software. The installed command will launch the server in a
141
+ detached process, and you can also start it yourself to customize behavior by
142
+ executing the server script directly.
143
+
144
+ `PORT` can be passed as an environment variable and defaults to 3000 if not
145
+ specified.
146
+
147
+ `NANO_POW_EFFORT` can also be passed as an environment variable to increase
148
+ or decrease the demand on the GPU and defaults to 8 if not specified.
149
+
150
+ ```console
151
+ $ # Launch the server and detach from the current session
152
+ $ PORT=8080 nano-pow --server
153
+ $ # View process ID for "NanoPow Server"
154
+ $ cat ~/.nano-pow/server.pid
155
+ $ # Display list of server logs
156
+ $ ls ~/.nano-pow/logs/
157
+ $ # Find process ID manually
158
+ $ ps aux | grep NanoPow
159
+ ```
160
+ Work is generated or validated by sending an HTTP `POST` request to the
161
+ configured hostname or IP address of the machine. Some basic help is available
162
+ via `GET` request.
163
+ ```console
164
+ $ # Generate a work value
165
+ $ curl -d '{
166
+ "action": "work_generate",
167
+ "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
168
+ }' localhost:3000
169
+ ```
170
+ ```console
171
+ $ # Validate a work value
172
+ $ curl -d '{
173
+ "action": "work_validate",
174
+ "work": "e45835c3b291c3d1",
175
+ "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
176
+ }' localhost:3000
92
177
  ```
93
178
 
94
179
  ## Notes
@@ -106,21 +191,16 @@ for all other accounts.
106
191
  * 𝘵𝘩𝘳𝘦𝘴𝘩𝘰𝘭𝘥 is 0xFFFFFFF800000000 for send/change blocks and 0xFFFFFE0000000000
107
192
  for receive/open/epoch blocks.
108
193
 
109
- The threshold is implemented in code as only the first 32 bits due to WGSL only
110
- supporting u32 integer types, but the result is the same. For example, if
111
- checking whether a two-digit number xx > 55, and it is known that the first
112
- digit is 6, it is also automatically known that xx is greater than 55. The
113
- default threshold used is the send/change difficulty. Any other threshold can be
114
- specified in practice.
115
-
116
194
  The BLAKE2b implementation has been optimized to the extreme for this package
117
- due to the very narrow use case to which it is applied. The compute shader is
118
- consequently immense, but the goal is to squeeze every last bit of speed and
119
- performance out of it.
195
+ due to the very narrow use case to which it is applied. The compute shader used
196
+ by the WebGPU implementation is consequently immense, but the goal is to squeeze
197
+ every last bit of speed and performance out of it.
120
198
 
121
199
  ## Tests
122
- `test.html` in the source repository contains some basic tests to compare the
123
- speed of this tool. Feel free to check out how your system fares.
200
+ A few basic tests are availabe in the source repository.
201
+ * `test/index.html` in the source repository contains a web interface to change
202
+ execution options and compare results.
203
+ * `test/script.sh` starts the `nano-pow` server and sends some basic requests.
124
204
 
125
205
  ## Building
126
206
  1. Clone source
@@ -128,11 +208,10 @@ speed of this tool. Feel free to check out how your system fares.
128
208
  1. Install dev dependencies
129
209
  1. Compile, minify, and bundle
130
210
 
131
- ```bash
132
- git clone https://zoso.dev/nano-pow.git
133
- cd nano-pow
134
- npm i
135
- npm run build
211
+ ```console
212
+ $ git clone https://zoso.dev/nano-pow.git
213
+ $ cd nano-pow
214
+ $ npm i
136
215
  ```
137
216
 
138
217
  ## Reporting Bugs
package/dist/bin/cli.js CHANGED
@@ -1,161 +1,136 @@
1
1
  #!/usr/bin/env node
2
2
  //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
3
3
  //! SPDX-License-Identifier: GPL-3.0-or-later
4
- import * as crypto from "node:crypto";
5
- import * as fs from "node:fs/promises";
6
4
  import * as readline from "node:readline/promises";
7
- import * as puppeteer from "puppeteer";
8
- import { cliHelp } from "../../docs/index.js";
5
+ process.title = "NanoPow CLI";
6
+ process.env.NANO_POW_DEBUG = "";
7
+ process.env.NANO_POW_EFFORT = "";
8
+ process.env.NANO_POW_PORT = "3000";
9
+ function log(...args2) {
10
+ if (process.env.NANO_POW_DEBUG) console.log(new Date(Date.now()).toLocaleString(), "NanoPow", args2);
11
+ }
9
12
  const hashes = [];
10
13
  const stdinErrors = [];
11
14
  if (!process.stdin.isTTY) {
12
15
  const stdin = readline.createInterface({
13
16
  input: process.stdin
14
17
  });
18
+ let i = 0;
15
19
  for await (const line of stdin) {
20
+ i++;
16
21
  if (/^[0-9A-Fa-f]{64}$/.test(line)) {
17
22
  hashes.push(line);
18
23
  } else {
19
- stdinErrors.push(`Skipping invalid stdin input: ${line}`);
24
+ stdinErrors.push(`Skipping invalid stdin input line ${i}`);
20
25
  }
21
26
  }
22
27
  }
23
28
  const args = process.argv.slice(2);
24
29
  if (hashes.length === 0 && args.length === 0 || args.some((v) => v === "--help" || v === "-h")) {
25
- console.log(cliHelp);
26
- process.exit();
30
+ console.log(
31
+ `Usage: nano-pow [OPTION]... BLOCKHASH...
32
+ Generate work for BLOCKHASH, or multiple work values for BLOCKHASH(es)
33
+ BLOCKHASH is a 64-character hexadecimal string. Multiple blockhashes must be separated by whitespace or line breaks.
34
+ Prints the result as a Javascript object to standard output as soon as it is calculated.
35
+ If using --batch, results are printed only after all BLOCKHASH(es) have be processed.
36
+ If using --validate, results will also include validity properties.
37
+
38
+ -h, --help show this dialog
39
+ --debug enable additional logging output
40
+
41
+ -b, --batch process all data before returning final results as array
42
+ -d, --difficulty=<value> override the minimum difficulty value
43
+ -e, --effort=<value> increase demand on GPU processing
44
+ -v, --validate=<value> check an existing work value instead of searching for one
45
+
46
+ If validating a nonce, it must be a 16-character hexadecimal value.
47
+ Effort must be a decimal number between 1-32.
48
+ Difficulty must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF.
49
+
50
+ Report bugs: <bug-nano-pow@zoso.dev>
51
+ Full documentation: <https://www.npmjs.com/package/nano-pow>
52
+ `
53
+ );
54
+ process.exit(0);
27
55
  }
28
56
  const inArgs = [];
29
57
  while (/^[0-9A-Fa-f]{64}$/.test(args[args.length - 1] ?? "")) {
30
58
  inArgs.unshift(args.pop());
31
59
  }
32
60
  hashes.push(...inArgs);
33
- let fn = "work_generate";
34
- let work = "";
35
- let isJson = false;
36
- const options = {};
61
+ if (hashes.length === 0) {
62
+ console.error("Invalid block hash input");
63
+ process.exit(1);
64
+ }
65
+ let isBatch = false;
66
+ const body = {
67
+ action: "work_generate"
68
+ };
37
69
  for (let i = 0; i < args.length; i++) {
38
70
  switch (args[i]) {
39
71
  case "--validate":
40
72
  case "-v": {
41
73
  if (args[i + 1] == null) throw new Error("Missing argument for work validation");
42
74
  if (!/^[0-9A-Fa-f]{16}$/.test(args[i + 1])) throw new Error("Invalid work to validate");
43
- fn = "work_validate";
44
- work = `'${args[i + 1]}', `;
75
+ body.action = "work_validate";
76
+ body.work = args[i + 1];
45
77
  break;
46
78
  }
47
- case "--threshold":
48
- case "-t": {
49
- if (args[i + 1] == null) throw new Error("Missing argument for threshold");
50
- if (!/^[0-9A-Fa-f]{0,8}$/.test(args[i + 1])) throw new Error("Invalid threshold");
51
- options["threshold"] = parseInt(args[i + 1], 16);
79
+ case "--difficulty":
80
+ case "-d": {
81
+ if (args[i + 1] == null) throw new Error("Missing argument for difficulty");
82
+ if (!/^[0-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(args[i + 1])) throw new Error("Invalid difficulty");
83
+ body.difficulty = args[i + 1];
52
84
  break;
53
85
  }
54
86
  case "--effort":
55
87
  case "-e": {
56
88
  if (args[i + 1] == null) throw new Error("Missing argument for effort");
57
89
  if (!/^[0-9]{0,2}$/.test(args[i + 1])) throw new Error("Invalid effort");
58
- options["effort"] = parseInt(args[i + 1], 10);
90
+ process.env.NANO_POW_EFFORT = args[i + 1];
59
91
  break;
60
92
  }
61
- case "--debug":
62
- case "-d": {
63
- options["debug"] = true;
93
+ case "--debug": {
94
+ process.env.NANO_POW_DEBUG = "true";
64
95
  break;
65
96
  }
66
- case "--json":
67
- case "-j": {
68
- isJson = true;
97
+ case "--batch":
98
+ case "-b": {
99
+ isBatch = true;
69
100
  break;
70
101
  }
71
102
  }
72
103
  }
73
- if (options["debug"]) {
74
- console.log(`NanoPowCli.${fn}()`);
75
- console.log(`${fn} options`, JSON.stringify(options));
76
- for (const stdinErr of stdinErrors) {
77
- console.warn(stdinErr);
78
- }
79
- }
80
- if (hashes.length === 0) {
81
- console.error("Invalid block hash input");
82
- process.exit(1);
104
+ log("CLI args:", ...args);
105
+ for (const stdinErr of stdinErrors) {
106
+ log(stdinErr);
83
107
  }
84
- (async () => {
85
- const NanoPow = await fs.readFile(new URL("../main.min.js", import.meta.url), "utf-8");
86
- const browser = await puppeteer.launch({
87
- headless: true,
88
- args: [
89
- "--headless=new",
90
- "--use-angle=vulkan",
91
- "--enable-features=Vulkan",
92
- "--disable-vulkan-surface",
93
- "--enable-unsafe-webgpu"
94
- ]
95
- });
96
- const page = await browser.newPage();
97
- const path = new URL(import.meta.url).pathname;
98
- const dir = path.slice(0, path.lastIndexOf("/"));
99
- await fs.writeFile(`${dir}/cli.html`, "");
100
- await page.goto(import.meta.resolve("./cli.html"));
101
- await page.waitForFunction(async () => {
102
- return await navigator.gpu.requestAdapter();
103
- });
104
- const inject = `
105
- ${NanoPow}
106
- window.results = []
107
- const hashes = ["${hashes.join('","')}"]
108
- for (const hash of hashes) {
109
- try {
110
- const result = await NanoPow.${fn}(${work}hash, ${JSON.stringify(options)})
111
- window.results.push(result)
112
- console.log(\`cli \${JSON.stringify(result, null, 4)}\`)
113
- } catch (err) {
114
- console.error(\`cli \${err}\`)
115
- }
116
- }
117
- console.log('cli exit')
118
- `;
119
- const hash = await crypto.subtle.digest("SHA-256", Buffer.from(inject, "utf-8"));
120
- const src = `sha256-${Buffer.from(hash).toString("base64")}`;
121
- let start = performance.now();
122
- page.on("console", async (msg) => {
123
- const output = msg.text().split(/^cli /);
124
- if (output[0] === "") {
125
- if (output[1] === "exit") {
126
- if (isJson) {
127
- const results = await page.evaluate(() => {
128
- return window.results;
129
- });
130
- console.log(JSON.stringify(results, null, 4));
131
- }
132
- const end = performance.now();
133
- if (options["debug"]) console.log(end - start, "ms total |", (end - start) / hashes.length, "ms avg");
134
- await browser.close();
135
- } else if (!isJson) {
136
- try {
137
- console.log(JSON.parse(output[1]));
138
- } catch (err) {
139
- console.log(output[1]);
140
- }
141
- }
142
- } else if (options["debug"]) {
143
- try {
144
- console.log(JSON.parse(msg.text()));
145
- } catch (err) {
146
- console.log(msg.text());
147
- }
108
+ log("Starting NanoPow CLI");
109
+ await import("./server.js");
110
+ const results = [];
111
+ const start = performance.now();
112
+ const aborter = new AbortController();
113
+ for (const hash of hashes) {
114
+ try {
115
+ body.hash = hash;
116
+ const kill = setTimeout(aborter.abort, 5e3);
117
+ const response = await fetch(`http://localhost:${process.env.NANO_POW_PORT}`, {
118
+ method: "POST",
119
+ body: JSON.stringify(body),
120
+ signal: aborter.signal
121
+ });
122
+ clearTimeout(kill);
123
+ const result = await response.json();
124
+ if (isBatch) {
125
+ results.push(result);
126
+ } else {
127
+ console.log(result);
148
128
  }
149
- });
150
- start = performance.now();
151
- await page.setContent(`
152
- <!DOCTYPE html>
153
- <head>
154
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'; script-src '${src}';">
155
- <script type="module">${inject}</script>
156
- </head>
157
- </html>
158
- `);
159
- await fs.unlink(`${dir}/cli.html`);
160
- if (options["debug"]) console.log("Puppeteer initialized");
161
- })();
129
+ } catch (err) {
130
+ log(err);
131
+ }
132
+ }
133
+ const end = performance.now();
134
+ if (isBatch) console.log(results);
135
+ log(end - start, "ms total |", (end - start) / hashes.length, "ms avg");
136
+ process.exit(0);
@@ -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 +%s).log 2>&1 & echo "$!" > "$NANO_POW_HOME"/server.pid;
13
+ node "$SCRIPT_DIR"/server.js > "$NANO_POW_LOGS"/nano-pow-server-$(date +%s).log 2>&1 & echo "$!" > "$NANO_POW_HOME"/server.pid;
14
14
  else
15
15
  node "$SCRIPT_DIR"/cli.js "$@";
16
16
  fi;
@@ -1,181 +1,165 @@
1
1
  #!/usr/bin/env node
2
2
  //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
3
3
  //! SPDX-License-Identifier: GPL-3.0-or-later
4
- import * as crypto from "node:crypto";
5
- import * as dns from "node:dns/promises";
6
- import * as fs from "node:fs/promises";
4
+ import { readFile, unlink, writeFile } from "node:fs/promises";
5
+ import { launch } from "puppeteer";
6
+ import { lookup } from "node:dns/promises";
7
7
  import * as http from "node:http";
8
- import * as os from "node:os";
9
- import * as puppeteer from "puppeteer";
10
- import { serverHelp } from "../../docs/index.js";
11
- const PORT = process.env.PORT || 3e3;
12
- function log(...args) {
13
- console.log(new Date(Date.now()).toLocaleString(), "NanoPow", args);
14
- }
15
- log("Starting server");
16
- const NanoPow = await fs.readFile(new URL("../main.min.js", import.meta.url), "utf-8");
8
+ import { hostname } from "node:os";
9
+ import { join } from "node:path";
10
+ process.title = "NanoPow Server";
11
+ const MAX_REQUEST_SIZE = 1024;
12
+ const MAX_BODY_SIZE = 158;
13
+ const DEBUG = !!(process.env.NANO_POW_DEBUG || false);
14
+ const EFFORT = +(process.env.NANO_POW_EFFORT || 8);
15
+ const PORT = +(process.env.NANO_POW_PORT || 3e3);
17
16
  let browser;
18
17
  let page;
19
- async function work_generate(res, json) {
20
- if (!/^[0-9A-Fa-f]{64}$/.test(json.hash ?? "")) {
21
- res.writeHead(400, { "Content-Type": "application/json" });
22
- res.end(JSON.stringify({ error: "Invalid hash. Must be a 64-character hex string." }));
23
- return;
24
- }
25
- if (json.difficulty && !/^[1-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(json.difficulty)) {
26
- res.writeHead(400, { "Content-Type": "application/json" });
27
- res.end(JSON.stringify({ error: "Invalid difficulty. Must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF." }));
28
- return;
29
- }
18
+ function log(...args) {
19
+ if (DEBUG) console.log(new Date(Date.now()).toLocaleString(), "NanoPow", args);
20
+ }
21
+ async function respond(res, data) {
22
+ let statusCode = 500;
23
+ let headers = { "Content-Type": "application/json" };
24
+ let response = "work_validate failed";
30
25
  try {
31
- const result = await page.evaluate(async (args) => {
32
- const options = {
33
- debug: true
34
- };
35
- if (args.difficulty) options.threshold = BigInt(`0x${args.difficulty}`);
36
- return await window.NanoPow.work_generate(args.hash, options);
37
- }, json);
38
- res.writeHead(200, { "Content-Type": "application/json" });
39
- res.end(JSON.stringify(result));
26
+ const datastring = Buffer.concat(data).toString().replace(/\s+/g, "");
27
+ if (Buffer.byteLength(datastring) > MAX_BODY_SIZE) {
28
+ throw new Error("Invalid data.");
29
+ }
30
+ const { action, hash: hash2, work, difficulty } = JSON.parse(datastring);
31
+ if (action !== "work_generate" && action !== "work_validate") {
32
+ throw new Error("Invalid action. Must be work_generate or work_validate.");
33
+ }
34
+ if (!/^[0-9A-Fa-f]{64}$/.test(hash2 ?? "")) {
35
+ throw new Error("Invalid hash. Must be a 64-character hex string.");
36
+ }
37
+ if (difficulty && !/^[1-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(difficulty)) {
38
+ throw new Error("Invalid difficulty. Must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF.");
39
+ }
40
+ if (action === "work_validate" && !/^[0-9A-Fa-f]{16}$/.test(work ?? "")) {
41
+ throw new Error("Invalid work. Must be a 16-character hex string.");
42
+ }
43
+ const options = {
44
+ debug: DEBUG,
45
+ effort: EFFORT,
46
+ threshold: difficulty
47
+ };
48
+ const args = [];
49
+ if (work) args.push(work);
50
+ args.push(hash2);
51
+ args.push(options);
52
+ response = JSON.stringify(await page.evaluate(async (action2, args2) => {
53
+ if (window.NanoPow == null) throw new Error("NanoPow not found");
54
+ return await window.NanoPow[action2](...args2);
55
+ }, action, args));
56
+ statusCode = 200;
40
57
  } catch (err) {
41
- log("work_generate error:", err);
42
- res.writeHead(500, { "Content-Type": "application/json" });
43
- res.end(JSON.stringify({ error: "work_generate failed" }));
58
+ log(err);
59
+ statusCode = 400;
60
+ } finally {
61
+ res.writeHead(statusCode, headers).end(response);
44
62
  }
45
63
  }
46
- async function work_validate(res, json) {
47
- if (!/^[0-9A-Fa-f]{64}$/.test(json.hash ?? "")) {
48
- res.writeHead(400, { "Content-Type": "application/json" });
49
- res.end(JSON.stringify({ error: "Invalid hash. Must be a 64-character hex string." }));
50
- return;
51
- }
52
- if (json.difficulty && !/^[1-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(json.difficulty)) {
53
- res.writeHead(400, { "Content-Type": "application/json" });
54
- res.end(JSON.stringify({ error: "Invalid difficulty. Must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF." }));
55
- return;
56
- }
57
- if (!/^[0-9A-Fa-f]{16}$/.test(json.work ?? "")) {
58
- res.writeHead(400, { "Content-Type": "application/json" });
59
- res.end(JSON.stringify({ error: "Invalid work. Must be a 16-character hex string." }));
60
- return;
64
+ const server = http.createServer((req, res) => {
65
+ let data = [];
66
+ let reqSize = 0;
67
+ if (req.method === "POST") {
68
+ req.on("data", (chunk) => {
69
+ reqSize += chunk.byteLength;
70
+ if (reqSize > MAX_REQUEST_SIZE) {
71
+ res.writeHead(413, { "Content-Type": "text/plain" });
72
+ res.end("Content Too Large");
73
+ req.socket.destroy();
74
+ return;
75
+ }
76
+ data.push(chunk);
77
+ });
78
+ req.on("end", async () => {
79
+ if (!req.socket.destroyed) {
80
+ await respond(res, data);
81
+ }
82
+ });
83
+ } else {
84
+ res.writeHead(200, { "Content-Type": "text/plain" });
85
+ res.end(
86
+ `Usage: Send POST request to server URL to generate or validate Nano proof-of-work
87
+
88
+ Generate work for a BLOCKHASH with an optional DIFFICULTY:
89
+ curl -d '{ action: "work_generate", hash: BLOCKHASH, difficulty?: DIFFICULTY }'
90
+
91
+ Validate WORK previously calculated for a BLOCKHASH with an optional DIFFICULTY:
92
+ curl -d '{ action: "work_validate", work: WORK, hash: BLOCKHASH, difficulty?: DIFFICULTY }'
93
+
94
+ BLOCKHASH is a 64-character hexadecimal string.
95
+ WORK is 16-character hexadecimal string.
96
+ DIFFICULTY is a 16-character hexadecimal string (default: FFFFFFF800000000)
97
+
98
+ Report bugs: <bug-nano-pow@zoso.dev>
99
+ Full documentation: <https://www.npmjs.com/package/nano-pow>
100
+ `
101
+ );
61
102
  }
103
+ });
104
+ server.on("error", (e) => {
105
+ log("Server error", e);
62
106
  try {
63
- const result = await page.evaluate(async (args) => {
64
- const options = {
65
- debug: true
66
- };
67
- if (args.difficulty) options.threshold = BigInt(`0x${args.difficulty}`);
68
- return await window.NanoPow.work_validate(args.work, args.hash, options);
69
- }, json);
70
- res.writeHead(200, { "Content-Type": "application/json" });
71
- res.end(JSON.stringify(result));
107
+ shutdown();
72
108
  } catch (err) {
73
- log("work_validate error:", err);
74
- res.writeHead(500, { "Content-Type": "application/json" });
75
- res.end(JSON.stringify({ error: "work_validate failed" }));
109
+ log("Failed to shut down", err);
110
+ process.exit(1);
76
111
  }
77
- }
78
- (async () => {
79
- browser = await puppeteer.launch({
80
- headless: true,
81
- args: [
82
- "--headless=new",
83
- "--use-angle=vulkan",
84
- "--enable-features=Vulkan",
85
- "--disable-vulkan-surface",
86
- "--enable-unsafe-webgpu"
87
- ]
88
- });
89
- page = await browser.newPage();
90
- page.on("console", (msg) => {
91
- log(msg.text());
92
- });
93
- const path = new URL(import.meta.url).pathname;
94
- const dir = path.slice(0, path.lastIndexOf("/"));
95
- await fs.writeFile(`${dir}/server.html`, "");
96
- await page.goto(import.meta.resolve("./server.html"));
97
- await page.waitForFunction(async () => {
98
- return await navigator.gpu.requestAdapter();
99
- });
100
- const inject = `${NanoPow};window.NanoPow=NanoPow;`;
101
- const hash = await crypto.subtle.digest("SHA-256", Buffer.from(inject, "utf-8"));
102
- const src = `sha256-${Buffer.from(hash).toString("base64")}`;
103
- await page.setContent(`
104
- <!DOCTYPE html>
105
- <head>
106
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'; script-src '${src}';">
107
- <script type="module">${inject}</script>
108
- </head>
109
- </html>
110
- `);
111
- await fs.unlink(`${dir}/server.html`);
112
- log("Puppeteer initialized");
113
- const server = http.createServer(async (req, res) => {
114
- let data = [];
115
- if (req.method === "POST") {
116
- req.on("data", (chunk) => {
117
- data.push(chunk);
118
- });
119
- req.on("end", async () => {
120
- let json;
121
- try {
122
- json = JSON.parse(Buffer.concat(data).toString());
123
- } catch (err) {
124
- log("JSON.parse error:", err);
125
- log("Failed JSON:", json);
126
- res.writeHead(400, { "Content-Type": "application/json" });
127
- res.end(JSON.stringify({ error: "Invalid data." }));
128
- return;
129
- }
130
- switch (json.action) {
131
- case "work_generate": {
132
- await work_generate(res, json);
133
- break;
134
- }
135
- case "work_validate": {
136
- await work_validate(res, json);
137
- break;
138
- }
139
- default: {
140
- res.writeHead(400, { "Content-Type": "application/json" });
141
- res.end(JSON.stringify({ error: `Invalid data.` }));
142
- return;
143
- }
144
- }
145
- });
146
- } else {
147
- res.writeHead(200, { "Content-Type": "text/plain" });
148
- res.end(serverHelp);
149
- }
112
+ });
113
+ function shutdown() {
114
+ log("Shutdown signal received");
115
+ const kill = setTimeout(() => {
116
+ log("Server unresponsive, forcefully stopped");
117
+ process.exit(1);
118
+ }, 1e4);
119
+ server.close(() => {
120
+ clearTimeout(kill);
121
+ log("Server stopped");
122
+ process.exit(0);
150
123
  });
151
- server.on("error", (e) => {
152
- log("Server error", e);
153
- try {
154
- shutdown();
155
- } catch (err) {
156
- log("Failed to shut down", err);
157
- process.exit(1);
158
- }
159
- });
160
- server.listen(PORT, async () => {
161
- process.title = "NanoPow Server";
162
- const ip = await dns.lookup(os.hostname(), { family: 4 });
163
- log(`Server process ${process.pid} running at ${ip.address}:${PORT}/`);
164
- });
165
- function shutdown() {
166
- log("Shutdown signal received");
167
- const kill = setTimeout(() => {
168
- log("Server unresponsive, forcefully stopped");
169
- process.exit(1);
170
- }, 1e4);
171
- server.close(async () => {
172
- await page?.close();
173
- await browser?.close();
174
- clearTimeout(kill);
175
- log("Server stopped");
176
- process.exit(0);
177
- });
178
- }
179
- process.on("SIGINT", shutdown);
180
- process.on("SIGTERM", shutdown);
181
- })();
124
+ }
125
+ process.on("SIGINT", shutdown);
126
+ process.on("SIGTERM", shutdown);
127
+ log("Starting NanoPow work server");
128
+ const NanoPow = await readFile(new URL("../main.min.js", import.meta.url), "utf-8");
129
+ browser = await launch({
130
+ headless: true,
131
+ args: [
132
+ "--headless=new",
133
+ "--use-angle=vulkan",
134
+ "--enable-features=Vulkan",
135
+ "--disable-vulkan-surface",
136
+ "--enable-unsafe-webgpu"
137
+ ]
138
+ });
139
+ page = await browser.newPage();
140
+ page.on("console", (msg) => log(msg.text()));
141
+ const path = new URL(import.meta.url).pathname;
142
+ const dir = path.slice(0, path.lastIndexOf("/"));
143
+ const filename = join(dir, `${process.pid}.html`);
144
+ await writeFile(filename, "");
145
+ await page.goto(import.meta.resolve(filename));
146
+ await page.waitForFunction(async () => {
147
+ return await navigator["gpu"].requestAdapter();
148
+ });
149
+ const src = `${NanoPow};window.NanoPow=NanoPow;`;
150
+ const hash = await crypto.subtle.digest("SHA-256", Buffer.from(src));
151
+ const enc = `sha256-${Buffer.from(hash).toString("base64")}`;
152
+ await page.setContent(`
153
+ <!DOCTYPE html>
154
+ <head>
155
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'; script-src '${enc}';">
156
+ <script type="module">${src}</script>
157
+ </head>
158
+ </html>
159
+ `);
160
+ await unlink(filename);
161
+ log("Puppeteer initialized");
162
+ server.listen(PORT, async () => {
163
+ const ip = await lookup(hostname(), { family: 4 });
164
+ log(`Server process ${process.pid} running at ${ip.address}:${PORT}/`);
165
+ });
package/dist/main.min.js CHANGED
@@ -51,10 +51,6 @@ var NanoPowGl = class _NanoPowGl {
51
51
  static #cores = Math.max(1, Math.floor(navigator.hardwareConcurrency));
52
52
  static #WORKLOAD = 256 * this.#cores;
53
53
  static #canvas;
54
- /** Drawing buffer size in pixels. */
55
- static get size() {
56
- return (this.#gl?.drawingBufferWidth ?? 0) * (this.#gl?.drawingBufferHeight ?? 0);
57
- }
58
54
  static #gl;
59
55
  static #drawProgram;
60
56
  static #downsampleProgram;
@@ -187,14 +183,14 @@ var NanoPowGl = class _NanoPowGl {
187
183
  this.#gl.uniformBlockBinding(this.#drawProgram, this.#gl.getUniformBlockIndex(this.#drawProgram, "WORK"), 1);
188
184
  this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null);
189
185
  this.#query = this.#gl.createQuery();
190
- this.#pixels = new Uint32Array(this.size * 4);
186
+ this.#pixels = new Uint32Array(this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight * 4);
191
187
  } catch (err) {
192
188
  throw new Error("WebGL initialization failed.", { cause: err });
193
189
  } finally {
194
190
  this.#busy = false;
195
191
  }
196
192
  this.#isInitialized = true;
197
- console.log(`NanoPow WebGL initialized at ${this.#gl.drawingBufferWidth}x${this.#gl.drawingBufferHeight}. Maximum nonces checked per frame: ${this.size}`);
193
+ console.log(`NanoPow WebGL initialized. Maximum nonces checked per frame: ${this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight}`);
198
194
  }
199
195
  /**
200
196
  * On WebGL context loss, attempts to clear all program variables and then
package/dist/types.d.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  // SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
2
2
  // SPDX-License-Identifier: GPL-3.0-or-later
3
3
 
4
- import '@webgpu/types'
4
+ declare global {
5
+ interface Window {
6
+ NanoPow: typeof NanoPow
7
+ }
8
+ }
5
9
 
6
10
  /**
7
11
  * Used by work server for inbound requests to `work_generate`.
@@ -11,6 +15,7 @@ import '@webgpu/types'
11
15
  * @param {string} [difficulty=FFFFFFF800000000] - Minimum threshold for a nonce to be valid
12
16
  */
13
17
  type WorkGenerateRequest = {
18
+ [key: string]: string | undefined
14
19
  action: 'work_generate'
15
20
  hash: string
16
21
  difficulty?: string
@@ -24,6 +29,7 @@ type WorkGenerateRequest = {
24
29
  * @param {string} difficulty - BLAKE2b hash result which was compared to specified minimum threshold
25
30
  */
26
31
  type WorkGenerateResponse = {
32
+ [key: string]: string
27
33
  hash: string
28
34
  work: string
29
35
  difficulty: string
@@ -38,6 +44,7 @@ type WorkGenerateResponse = {
38
44
  * @param {string} [difficulty=FFFFFFF800000000] - Minimum threshold for a nonce to be valid
39
45
  */
40
46
  type WorkValidateRequest = {
47
+ [key: string]: string | undefined
41
48
  action: 'work_validate'
42
49
  hash: string
43
50
  work: string
@@ -55,6 +62,7 @@ type WorkValidateRequest = {
55
62
  * @param {string} valid_receive - 1 for true if nonce is valid for receive blocks, else 0 for false
56
63
  */
57
64
  type WorkValidateResponse = {
65
+ [key: string]: string | undefined
58
66
  hash: string
59
67
  work: string
60
68
  difficulty: string
@@ -103,9 +111,8 @@ export type NanoPowOptions = {
103
111
  * Nano proof-of-work using WebGL 2.0.
104
112
  */
105
113
  export declare class NanoPowGl {
114
+ static [key: string]: (...args: any[]) => any
106
115
  #private
107
- /** Drawing buffer width in pixels. */
108
- static get size (): number
109
116
  /**
110
117
  * Constructs canvas, gets WebGL context, initializes buffers, and compiles
111
118
  * shaders.
@@ -153,6 +160,7 @@ export declare class NanoPowGl {
153
160
  */
154
161
  export declare class NanoPowGpu {
155
162
  #private
163
+ static [key: string]: (...args: any[]) => any
156
164
  static init (): Promise<void>
157
165
  static setup (): void
158
166
  static reset (): void
package/docs/nano-pow.1 CHANGED
@@ -12,12 +12,19 @@ nano-pow \- proof-of-work generation and validation for Nano cryptocurrency
12
12
  .SH DESCRIPTION
13
13
  Generate work for \fIBLOCKHASH\fR, or multiple work values for \fIBLOCKHASH\fR(es).
14
14
  .PP
15
+ If the \fI--server\fR option is provided as the first argument, all other options are ignored and the NanoPow server is started.
16
+ .PP
15
17
  \fIBLOCKHASH\fR is a 64-character hexadecimal string. Multiple blockhashes must be separated by whitespace or line breaks.
16
18
  .PP
17
- Prints a 16-character hexadecimal work value to standard output. If \fB--validate\fR is used, prints 'true' or 'false' to standard output.
19
+ Prints a 16-character hexadecimal work value, the BLAKE2b result, and the originating hash to standard output as a Javascript object.
20
+ .PP
21
+ If \fB--validate\fR is used, the original work value is returned instead along with validation flags.
18
22
 
19
23
  .SH OPTIONS
20
24
  .TP
25
+ \fB--server\fR
26
+ Start work server (see SERVER below). Must be the first argument in order to be recognized.
27
+ .TP
21
28
  \fB\-h\fR, \fB\-\-help\fR
22
29
  Show this help dialog and exit.
23
30
  .TP
@@ -25,20 +32,64 @@ Show this help dialog and exit.
25
32
  Enable additional logging output.
26
33
  .TP
27
34
  \fB\-j\fR, \fB\-\-json\fR
28
- Format output as JSON.
35
+ Format final output of all hashes as a JSON array of stringified values instead of incrementally returning Javascript objects for each result.
29
36
  .TP
30
37
  \fB\-e\fR, \fB\-\-effort\fR=\fIEFFORT\fR
31
38
  Increase demand on GPU processing. Must be between 1 and 32 inclusive.
32
39
  .TP
33
40
  \fB\-t\fR, \fB\-\-threshold\fR=\fITHRESHOLD\fR
34
- Override the minimum threshold value. Higher values increase difficulty. Must be a hexadecimal string between 0 and FFFFFFFF inclusive.
41
+ Override the minimum threshold value. Higher values increase difficulty. Must be a hexadecimal string between 1 and FFFFFFFFFFFFFFFF inclusive.
35
42
  .TP
36
43
  \fB\-v\fR, \fB\-\-validate\fR=\fIWORK\fR
37
- Check an existing work value instead of searching for one.
44
+ Check an existing work value instead of searching for one. If you pass multiple blockhashes, the work value will be validated against each one.
45
+
46
+ .SH SERVER
47
+ Calling \fBnano-pow\fR with the \fI--server\fR option will start the NanoPow work server in a detached process.
48
+ .PP
49
+ It provides work generation and validation via HTTP requests in a similar, but not identical, fashion as the official Nano node.
50
+ .PP
51
+ More specifically, it does not support the \fIuse_peers\fR, \fImultiplier\fR, \fIaccount\fR, \fIversion\fR, \fIblock\fR, and \fIjson_block\fR options.
52
+ .PP
53
+ By default, the server listens on port 3000. To use a different port, set the \fBPORT\fR environment variable before starting the server.
54
+
55
+ .PP
56
+ .EX
57
+ $ PORT=8080 nano-pow --server
58
+ .EE
59
+
60
+ .PP
61
+ The server process ID (PID) is saved to \fB~/.nano-pow/server.pid\fR. Log files are stored in \fB~/.nano-pow/logs/\fR.
62
+ .TP
63
+ To stop the server, terminate it using its PID.
64
+ .EX
65
+ $ cat ~/.nano-pow/server.pid
66
+ 12345
67
+ $ kill 12345
68
+ .EE
69
+ .TP
70
+ Alternatively, use \fBpgrep\fR to find the PID by process name:
71
+ .EX
72
+ $ pgrep "NanoPow Server"
73
+ 12345
74
+ $ kill $(pgrep "NanoPow Server")
75
+ .EE
76
+
77
+ .PP
78
+ The server accepts HTTP \fBPOST\fR requests to the server's root path (\fB/\fR). Requests should be in JSON format and include an \fBaction\fR field to specify the desired operation.
79
+ .PP
80
+ The following actions are supported:
81
+ .TP
82
+ \fBwork_generate\fR
83
+ Generate a work value for a given blockhash. Requires a \fBhash\fR field containing the 64-character hexadecimal blockhash.
84
+ .TP
85
+ \fBwork_validate\fR
86
+ Validate an existing work value against a blockhash. Requires \fBwork\fR field containing the 16-character hexadecimal work value and a \fBhash\fR field containing the 64-character hexadecimal blockhash.
87
+ .PP
88
+
38
89
 
39
- .SH EXAMPLES
90
+ .SH EXAMPLES - CLI
40
91
  .PP
41
- Search for a work nonce for a blockhash using the default threshold 0xFFFFFFF8:
92
+ Search for a work nonce for a blockhash using the default threshold 0xFFFFFFF800000000:
42
93
  .EX
43
94
  $ nano-pow \fB0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\fR
44
95
  .EE
@@ -46,7 +97,7 @@ $ nano-pow \fB0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\f
46
97
  .PP
47
98
  Search for a work nonce using a custom threshold and increased effort:
48
99
  .EX
49
- $ nano-pow \fB\-t fffffe00 \-e 32 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\fR
100
+ $ nano-pow \fB\-t fffffe0000000000 \-e 32 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\fR
50
101
  .EE
51
102
 
52
103
  .PP
@@ -61,6 +112,26 @@ Validate an existing work nonce against a blockhash and show debugging output:
61
112
  $ nano-pow \fB\-d \-v fedcba9876543210 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\fR
62
113
  .EE
63
114
 
115
+ .SH EXAMPLES - SERVER
116
+ .PP
117
+ Generate a work value:
118
+ .EX
119
+ $ curl -d '{
120
+ "action": "work_generate",
121
+ "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
122
+ }' localhost:3000
123
+ .EE
124
+
125
+ .PP
126
+ Validate a work value:
127
+ .EX
128
+ $ curl -d '{
129
+ "action": "work_validate",
130
+ "work": "e45835c3b291c3d1",
131
+ "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
132
+ }' localhost:3000
133
+ .EE
134
+
64
135
  .SH AUTHOR
65
136
  Written by Chris Duncan.
66
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-pow",
3
- "version": "4.0.6",
3
+ "version": "4.0.8",
4
4
  "description": "Proof-of-work generation and validation with WebGPU/WebGL for Nano cryptocurrency.",
5
5
  "keywords": [
6
6
  "nemo",
package/docs/index.js DELETED
@@ -1,38 +0,0 @@
1
- //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
2
- //! SPDX-License-Identifier: GPL-3.0-or-later
3
-
4
- export const cliHelp = `Usage: nano-pow [OPTION]... BLOCKHASH...
5
- Generate work for BLOCKHASH, or multiple work values for BLOCKHASH(es)
6
- BLOCKHASH is a 64-character hexadecimal string. Multiple blockhashes must be separated by whitespace or line breaks.
7
- Prints a 16-character hexadecimal work value to standard output. If using --validate, prints 'true' or 'false' to standard output instead.
8
-
9
- -h, --help show this dialog
10
- -d, --debug enable additional logging output
11
- -j, --json format output as JSON
12
- -e, --effort=<value> increase demand on GPU processing
13
- -t, --threshold=<value> override the minimum threshold value
14
- -v, --validate=<value> check an existing work value instead of searching for one
15
-
16
- If validating a nonce, it must be a 16-character hexadecimal value.
17
- Effort must be a decimal number between 1-32.
18
- Threshold must be a hexadecimal string between 0-FFFFFFFF.
19
-
20
- Report bugs: <bug-nano-pow@zoso.dev>
21
- Full documentation: <https://www.npmjs.com/package/nano-pow>
22
- `
23
-
24
- export const serverHelp = `Usage: Send POST request to server URL to generate or validate Nano proof-of-work
25
-
26
- Generate work for a BLOCKHASH with an optional DIFFICULTY:
27
- curl -d '{ action: "work_generate", hash: BLOCKHASH, difficulty?: DIFFICULTY }'
28
-
29
- Validate WORK previously calculated for a BLOCKHASH with an optional DIFFICULTY:
30
- curl -d '{ action: "work_validate", work: WORK, hash: BLOCKHASH, difficulty?: DIFFICULTY }'
31
-
32
- BLOCKHASH is a 64-character hexadecimal string.
33
- WORK is 16-character hexadecimal string.
34
- DIFFICULTY is a 16-character hexadecimal string (default: FFFFFFF800000000)
35
-
36
- Report bugs: <bug-nano-pow@zoso.dev>
37
- Full documentation: <https://www.npmjs.com/package/nano-pow>
38
- `