Add WASM and CPU implementations.
Add API options parameter.
Add scripts to generate shaders and AssemblyScript.
Add sample systemd service for server.
Add Logger and Queue utility classes.
Fix CLI and server IPC.
Add WorkErrorResponse type.
Improve type checking and config checking.
Move generate APIs into their own directory.
--- /dev/null
+{
+ "options": {
+ "outFile": "./src/lib/generate/wasm/asm/build/compute.wasm",
+ "optimizeLevel": 3,
+ "shrinkLevel": 2,
+ "converge": true,
+ "noAssert": true,
+ "uncheckedBehavior": "always",
+ "bindings": "esm",
+ "sourceMap": false,
+ "debug": false,
+ "runtime": "stub",
+ "exportRuntime": false,
+ "enable": [
+ "simd"
+ ],
+ "disable": [
+ "mutable-globals",
+ "sign-extension",
+ "nontrapping-f2i"
+ ],
+ "noColors": true
+ }
+}
--- /dev/null
+SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+SPDX-License-Identifier: GPL-3.0-or-later
- Chromium supports up to 5760x5760
- Firefox supports up to a whopping 8192x8192 which actually makes it competitive with WebGPU
-
## All Results
| Version | System | Browser | API | Total | Rate | Median | Mean |
|-----------|--------------|-------------|-----------|-----------|----------|----------|----------|
\# SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
\# SPDX-License-Identifier: GPL-3.0-or-later
-.TH nano-pow 1 2025-03-12 "nano-pow v3.1.0"
+.TH nano-pow 1 2025-06-14 "nano-pow v5.0.0"
.SH NAME
nano-pow \- proof-of-work generation and validation for Nano cryptocurrency
.SS ENVIRONMENT VARIABLES
.TP
-\fBNANO_POW_PORT\fR
-Override the default port on which the NanoPow server listens for requests.
+\fBNANO_POW_DEBUG\fR
+Enable additional logging saved to the \fBHOME\fR directory.
.TP
\fBNANO_POW_EFFORT\fR
Increase demand on GPU processing. Must be between 1 and 32 inclusive.
.TP
-\fBNANO_POW_DEBUG\fR
-Enable additional logging saved to the \fBHOME\fR directory.
+\fBNANO_POW_PORT\fR
+Override the default port on which the NanoPow server listens for requests.
.PP
The server process ID (PID) is saved to \fB~/.nano-pow/server.pid\fR. Log files are stored in \fB~/.nano-pow/logs/\fR.
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.
.PP
-
.SH EXAMPLES - CLI
.PP
Search for a work nonce for a blockhash using the default difficulty FFFFFFF800000000:
--- /dev/null
+# SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+## Example systemd service unit file to start the nano-pow server at login.
+## Run `systemctl --user edit --force --full nano-pow.service`
+## Copy contents below into editor, modifying ExecStart as needed, and save.
+## Run `systemctl --user enable nano-pow --now` to start it now and at boot.
+
+[Unit]
+Description=NanoPow Server
+
+[Service]
+# Must point at install path, often in `npm_config_prefix` directory:
+ExecStart=%h/.local/bin/nano-pow --server
+
+Type=forking
+WorkingDirectory=%h
+EnvironmentFile=-%h/.nano-pow/config
+PassEnvironment=NANO_POW_DEBUG NANO_POW_EFFORT NANO_POW_PORT
+PIDFile=%h/.nano-pow/server.pid
+StandardOutput=journal+console
+StandardError=journal+console
+
+[Install]
+WantedBy=default.target
//! SPDX-License-Identifier: GPL-3.0-or-later
import { build } from 'esbuild'
-import { glsl } from "esbuild-plugin-glsl"
+import { glsl } from 'esbuild-plugin-glsl'
await build({
bundle: true,
{ in: './src/types.d.ts', out: 'types.d' }
],
loader: {
- '.d.ts': 'copy'
+ '.d.ts': 'copy',
+ '.wasm': 'binary'
},
format: 'esm',
legalComments: 'inline',
"nano-pow": "dist/bin/nano-pow.sh"
},
"devDependencies": {
- "@types/node": "^22.15.14",
+ "@types/node": "^22.15.17",
"@webgpu/types": "^0.1.60",
+ "assemblyscript": "^0.27.36",
"esbuild": "^0.25.4",
"esbuild-plugin-glsl": "^1.4.0",
"typescript": "^5.8.3"
"url": "nano:nano_1zosoqs47yt47bnfg7sdf46kj7asn58b7uzm9ek95jw7ccatq37898u1zoso"
},
"optionalDependencies": {
- "puppeteer": "^24.8.1"
+ "puppeteer": "^24.8.2"
}
},
"node_modules/@babel/code-frame": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
- "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"license": "MIT",
"optional": true,
"dependencies": {
- "@babel/helper-validator-identifier": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
+ "picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
- "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"license": "MIT",
"optional": true,
"engines": {
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
- "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
+ "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [
"ppc64"
],
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
- "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
+ "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [
"arm"
],
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
- "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
+ "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [
"arm64"
],
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
- "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
+ "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [
"x64"
],
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
- "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
+ "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [
"arm64"
],
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
- "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
+ "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [
"x64"
],
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
- "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [
"arm64"
],
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
- "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
+ "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [
"x64"
],
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
- "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
+ "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [
"arm"
],
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
- "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
+ "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [
"arm64"
],
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
- "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
+ "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [
"ia32"
],
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
- "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
+ "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [
"loong64"
],
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
- "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
+ "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [
"mips64el"
],
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
- "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
+ "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [
"ppc64"
],
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
- "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
+ "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [
"riscv64"
],
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
- "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
+ "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [
"s390x"
],
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
- "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
+ "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
"cpu": [
"x64"
],
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
- "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
"cpu": [
"arm64"
],
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
- "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
+ "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [
"x64"
],
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
- "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
"cpu": [
"arm64"
],
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
- "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
+ "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [
"x64"
],
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
- "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
+ "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [
"x64"
],
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
- "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
+ "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [
"arm64"
],
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
- "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
+ "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [
"ia32"
],
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
- "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
+ "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [
"x64"
],
}
},
"node_modules/@puppeteer/browsers": {
- "version": "2.10.3",
- "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.3.tgz",
- "integrity": "sha512-iPpnFpX25gKIVsHsqVjHV+/GzW36xPgsscWkCnrrETndcdxNsXLdCrTwhkCJNR/FGWr122dJUBeyV4niz/j3TA==",
+ "version": "2.10.5",
+ "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
+ "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
- "debug": "^4.4.0",
+ "debug": "^4.4.1",
"extract-zip": "^2.0.1",
"progress": "^2.0.3",
"proxy-agent": "^6.5.0",
- "semver": "^7.7.1",
+ "semver": "^7.7.2",
"tar-fs": "^3.0.8",
"yargs": "^17.7.2"
},
"optional": true
},
"node_modules/@types/node": {
- "version": "22.15.14",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.14.tgz",
- "integrity": "sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==",
+ "version": "22.15.22",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.22.tgz",
+ "integrity": "sha512-IZ8lyY8xikZwGTJ9tsmbE68+mZbx2tsR+WnN1ZJU/h5flim8xBxEbpDrouMQNkMeT4pYxyJOTkf7YyDcQaUvQw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"license": "Python-2.0",
"optional": true
},
+ "node_modules/assemblyscript": {
+ "version": "0.27.36",
+ "resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.27.36.tgz",
+ "integrity": "sha512-1qX2zf6p7l/mNYv8r21jC/Yft7kX7XKR3xUHw41zvV4xad5lyC8w7jZiwZBGoy64VKZLc+bTDJDWi8Kb70YrHA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "binaryen": "116.0.0-nightly.20240114",
+ "long": "^5.2.4"
+ },
+ "bin": {
+ "asc": "bin/asc.js",
+ "asinit": "bin/asinit.js"
+ },
+ "engines": {
+ "node": ">=18",
+ "npm": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/assemblyscript"
+ }
+ },
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
"optional": true
},
"node_modules/bare-fs": {
- "version": "4.1.4",
- "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.4.tgz",
- "integrity": "sha512-r8+26Voz8dGX3AYpJdFb1ZPaUSM8XOLCZvy+YGpRTmwPHIxA7Z3Jov/oMPtV7hfRQbOnH8qGlLTzQAbgtdNN0Q==",
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz",
+ "integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"node": ">=10.0.0"
}
},
+ "node_modules/binaryen": {
+ "version": "116.0.0-nightly.20240114",
+ "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
+ "integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "wasm-opt": "bin/wasm-opt",
+ "wasm2js": "bin/wasm2js"
+ }
+ },
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
}
},
"node_modules/debug": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
- "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"optional": true,
"dependencies": {
}
},
"node_modules/esbuild": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
- "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
+ "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.4",
- "@esbuild/android-arm": "0.25.4",
- "@esbuild/android-arm64": "0.25.4",
- "@esbuild/android-x64": "0.25.4",
- "@esbuild/darwin-arm64": "0.25.4",
- "@esbuild/darwin-x64": "0.25.4",
- "@esbuild/freebsd-arm64": "0.25.4",
- "@esbuild/freebsd-x64": "0.25.4",
- "@esbuild/linux-arm": "0.25.4",
- "@esbuild/linux-arm64": "0.25.4",
- "@esbuild/linux-ia32": "0.25.4",
- "@esbuild/linux-loong64": "0.25.4",
- "@esbuild/linux-mips64el": "0.25.4",
- "@esbuild/linux-ppc64": "0.25.4",
- "@esbuild/linux-riscv64": "0.25.4",
- "@esbuild/linux-s390x": "0.25.4",
- "@esbuild/linux-x64": "0.25.4",
- "@esbuild/netbsd-arm64": "0.25.4",
- "@esbuild/netbsd-x64": "0.25.4",
- "@esbuild/openbsd-arm64": "0.25.4",
- "@esbuild/openbsd-x64": "0.25.4",
- "@esbuild/sunos-x64": "0.25.4",
- "@esbuild/win32-arm64": "0.25.4",
- "@esbuild/win32-ia32": "0.25.4",
- "@esbuild/win32-x64": "0.25.4"
+ "@esbuild/aix-ppc64": "0.25.5",
+ "@esbuild/android-arm": "0.25.5",
+ "@esbuild/android-arm64": "0.25.5",
+ "@esbuild/android-x64": "0.25.5",
+ "@esbuild/darwin-arm64": "0.25.5",
+ "@esbuild/darwin-x64": "0.25.5",
+ "@esbuild/freebsd-arm64": "0.25.5",
+ "@esbuild/freebsd-x64": "0.25.5",
+ "@esbuild/linux-arm": "0.25.5",
+ "@esbuild/linux-arm64": "0.25.5",
+ "@esbuild/linux-ia32": "0.25.5",
+ "@esbuild/linux-loong64": "0.25.5",
+ "@esbuild/linux-mips64el": "0.25.5",
+ "@esbuild/linux-ppc64": "0.25.5",
+ "@esbuild/linux-riscv64": "0.25.5",
+ "@esbuild/linux-s390x": "0.25.5",
+ "@esbuild/linux-x64": "0.25.5",
+ "@esbuild/netbsd-arm64": "0.25.5",
+ "@esbuild/netbsd-x64": "0.25.5",
+ "@esbuild/openbsd-arm64": "0.25.5",
+ "@esbuild/openbsd-x64": "0.25.5",
+ "@esbuild/sunos-x64": "0.25.5",
+ "@esbuild/win32-arm64": "0.25.5",
+ "@esbuild/win32-ia32": "0.25.5",
+ "@esbuild/win32-x64": "0.25.5"
}
},
"node_modules/esbuild-plugin-glsl": {
"license": "MIT",
"optional": true
},
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
}
},
"node_modules/puppeteer": {
- "version": "24.8.1",
- "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.8.1.tgz",
- "integrity": "sha512-5OvJCe6tQ09EWf35qqyoH/cr9YGMbLj0ZpoT2pEImF9Ox35JXyAn8kIqj8eBgpDfyzuEwXYIMUwIAIkdgO/gDA==",
+ "version": "24.9.0",
+ "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.9.0.tgz",
+ "integrity": "sha512-L0pOtALIx8rgDt24Y+COm8X52v78gNtBOW6EmUcEPci0TYD72SAuaXKqasRIx4JXxmg2Tkw5ySKcpPOwN8xXnQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
- "@puppeteer/browsers": "2.10.3",
+ "@puppeteer/browsers": "2.10.5",
"chromium-bidi": "5.1.0",
"cosmiconfig": "^9.0.0",
"devtools-protocol": "0.0.1439962",
- "puppeteer-core": "24.8.1",
+ "puppeteer-core": "24.9.0",
"typed-query-selector": "^2.12.0"
},
"bin": {
}
},
"node_modules/puppeteer-core": {
- "version": "24.8.1",
- "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.8.1.tgz",
- "integrity": "sha512-UP/VIxVk/Akrgql3a55ZAIuAIx7+yQevz6qEXFUtSTIynEcgsCJ6tlRdi7uKAAlovmNQG4iNMzq9f8WxZLnGGg==",
+ "version": "24.9.0",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.9.0.tgz",
+ "integrity": "sha512-HFdCeH/wx6QPz8EncafbCqJBqaCG1ENW75xg3cLFMRUoqZDgByT6HSueiumetT2uClZxwqj0qS4qMVZwLHRHHw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
- "@puppeteer/browsers": "2.10.3",
+ "@puppeteer/browsers": "2.10.5",
"chromium-bidi": "5.1.0",
- "debug": "^4.4.0",
+ "debug": "^4.4.1",
"devtools-protocol": "0.0.1439962",
"typed-query-selector": "^2.12.0",
"ws": "^8.18.2"
}
},
"node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"optional": true,
"bin": {
}
},
"node_modules/tar-fs": {
- "version": "3.0.8",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
- "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz",
+ "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==",
"license": "MIT",
"optional": true,
"dependencies": {
}
},
"node_modules/zod": {
- "version": "3.24.4",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz",
- "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
+ "version": "3.25.30",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.30.tgz",
+ "integrity": "sha512-VolhdEtu6TJr/fzGuHA/SZ5ixvXqA6ADOG9VRcQ3rdOKmF5hkmcJbyaQjUH5BgmpA9gej++zYRX7zjSmdReIwA==",
"license": "MIT",
"optional": true,
"funding": {
],
"main": "./dist/main.min.js",
"browser": {
- "./dist/main.min.js": true
+ "./dist/main.min.js": true,
+ "node:fs/promises": false,
+ "node:worker_threads": false
},
"bin": {
"nano-pow": "dist/bin/nano-pow.sh"
"url": "git+https://zoso.dev/nano-pow.git"
},
"scripts": {
- "benchmark": "npm run build && ./dist/bin/nano-pow.sh --benchmark 100",
- "build": "rm -rf {dist,types} && tsc && node esbuild.mjs && cp -p src/bin/nano-pow.sh dist/bin",
+ "asgenerate": "cd src/lib/generate/wasm/asm && rm -rf build && tsc && node build/generate.js > build/index.ts && cp tsconfig.json* build",
+ "benchmark": "npm run build && ./dist/bin/nano-pow.sh --benchmark 10 --debug",
+ "build": "rm -rf {dist,types} && tsc && npm run generate && asc src/lib/generate/wasm/asm/build/index.ts && node esbuild.mjs && cp -p src/bin/nano-pow.sh dist/bin",
+ "generate": "npm run asgenerate && npm run glgenerate && npm run gpugenerate",
+ "glgenerate": "cd src/lib/generate/webgl/shaders && rm -rf build && tsc && node build/generate.js > build/draw.frag && cp tsconfig.json* build",
+ "gpugenerate": "cd src/lib/generate/webgpu/shaders && rm -rf build && tsc && node build/generate.js > build/compute.wgsl && cp tsconfig.json* build",
"prepare": "npm run build",
"start": "./dist/bin/nano-pow.sh --server",
"test": "npm run build && ./test/script.sh"
},
"devDependencies": {
- "@types/node": "^22.15.14",
+ "@types/node": "^22.15.17",
"@webgpu/types": "^0.1.60",
+ "assemblyscript": "^0.27.36",
"esbuild": "^0.25.4",
"esbuild-plugin-glsl": "^1.4.0",
"typescript": "^5.8.3"
},
"optionalDependencies": {
- "puppeteer": "^24.8.1"
+ "puppeteer": "^24.8.2"
},
"type": "module",
"exports": {
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { spawn } from 'node:child_process'
+import { Serializable, spawn } from 'node:child_process'
import { getRandomValues } from 'node:crypto'
import { createInterface } from 'node:readline/promises'
-import { average, isHex, isNotHex, log } from '../utils'
-import { WorkGenerateResponse, WorkValidateResponse } from '#types'
+import { stats, Logger } from '#utils'
+import { WorkErrorResponse, WorkGenerateResponse, WorkValidateResponse } from '#types'
process.title = 'NanoPow CLI'
+const logger = new Logger()
delete process.env.NANO_POW_DEBUG
delete process.env.NANO_POW_EFFORT
let i = 0
for await (const line of stdin) {
i++
- if (isHex(line, 64)) {
+ if (/^[A-Fa-f\d]{64}$/.test(line)) {
hashes.push(line)
} else {
stdinErrors.push(`Skipping invalid stdin input line ${i}`)
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
Report bugs: <bug-nano-pow@zoso.dev>
Full documentation: <https://www.npmjs.com/package/nano-pow>
-`
- )
+`)
process.exit(0)
}
const inArgs: string[] = []
-while (isHex(args[args.length - 1], 64)) {
- inArgs.unshift(args.pop() as string)
+let isParsingHash: boolean = true
+while (isParsingHash) {
+ if (!/^[A-Fa-f\d]{16}$/.test(args[args.length - 1])) break
+ try {
+ inArgs.unshift(args.pop() as string)
+ } catch {
+ isParsingHash = false
+ }
}
hashes.push(...inArgs)
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
- case ('--validate'):
- case ('-v'): {
- const v = args[i + 1]
- if (v == null) throw new Error('Missing argument for work validation')
- if (isNotHex(v, 16)) throw new Error('Invalid work to validate')
- if (hashes.length !== 1) throw new Error('Validate accepts exactly one hash')
- body.action = 'work_validate'
- body.work = v
- break
- }
- case ('--difficulty'):
- case ('-d'): {
- const d = args[i + 1]
- if (d == null) throw new Error('Missing argument for difficulty')
- if (isNotHex(d, 1, 16)) throw new Error('Invalid difficulty')
- body.difficulty = d
- break
- }
- case ('--effort'):
- case ('-e'): {
- const e = args[i + 1]
- if (e == null) throw new Error('Missing argument for effort')
- if (parseInt(e) < 1 || parseInt(e) > 32) throw new Error('Invalid effort')
- process.env.NANO_POW_EFFORT = e
+ case ('--batch'):
+ case ('-b'): {
+ isBatch = true
break
}
case ('--benchmark'): {
break
}
case ('--debug'): {
+ logger.isEnabled = true
process.env.NANO_POW_DEBUG = 'true'
break
}
- case ('--batch'):
- case ('-b'): {
- isBatch = true
+ case ('--difficulty'):
+ case ('-d'): {
+ const d = args[i + 1]
+ if (d == null) throw new Error('Missing argument for difficulty')
+ if (!/^[A-Fa-f\d]{16}$/.test(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 ('--validate'):
+ case ('-v'): {
+ const v = args[i + 1]
+ if (v == null) throw new Error('Missing argument for work validation')
+ if (!/^[A-Fa-f\d]{16}$/.test(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
}
}
process.exit(1)
}
-log('CLI args:', ...args)
+logger.log('CLI args: ', ...args)
for (const stdinErr of stdinErrors) {
- log(stdinErr)
+ logger.log(stdinErr)
}
// Initialize server
-log('Starting NanoPow CLI')
+logger.log('launching server')
const server = spawn(
process.execPath,
)
server.once('error', err => {
- log(err)
+ logger.log(err)
process.exit(1)
})
-server.on('message', async (msg: { type: string, message: number | string }): Promise<void> => {
- if (msg.type === 'console') {
- log(msg.message)
- }
- if (msg.type === 'listening') {
- const port = +msg.message
- if (port > -1) {
- log(`CLI server listening on port ${port}`)
- try {
- await execute(port)
- } catch {
- log(`Error executing ${body.action}`)
+server.on('message', async (msg: Serializable): Promise<void> => {
+ 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 {
+ await execute()
+ } catch {
+ logger.log(`Error executing ${body.action}`)
+ }
+ } else {
+ logger.log('CLI server failed to connect over IPC')
}
- } else {
- log('Server failed to provide port')
}
+ } else {
+ logger.log('received invalid message from server', msg)
}
})
server.on('close', code => {
- log(`Server closed with exit code ${code}`)
+ logger.log(`Server closed with exit code ${code}`)
process.exit(code)
})
-async function execute (port: number): Promise<void> {
- // Execution must be sequential else GPU cannot map to CPU and will throw
- const results: (WorkGenerateResponse | WorkValidateResponse)[] = []
- if (isBenchmark) console.log('Running benchmark...')
+async function execute (): Promise<void> {
+ server.send(0)
+ const results: (WorkErrorResponse | WorkGenerateResponse | WorkValidateResponse)[] = []
+ if (isBenchmark) {
+ console.log('Running benchmark...')
+ }
let start = 0
const times: number[] = []
for (const hash of hashes) {
const kill = setTimeout(() => aborter.abort(), 60_000)
body.hash = hash
start = performance.now()
- const response = await fetch(`http://localhost:${port}`, {
- method: 'POST',
- body: JSON.stringify(body),
- signal: aborter.signal
+ const result: WorkGenerateResponse | WorkValidateResponse | WorkErrorResponse = await new Promise((resolve, reject): void => {
+ const listener = async (msg: Serializable): Promise<void> => {
+ 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(body) }, cb => cb ? reject(cb) : logger.log('cli ipc request to server'))
})
clearTimeout(kill)
- const result = await response.json()
if (isBatch || isBenchmark) {
results.push(result)
times.push(performance.now() - start)
console.log(result)
}
} catch (err) {
- log(err)
+ logger.log(err)
}
}
if (isBatch && !isBenchmark) console.log(results)
- if (process.env.NANO_POW_DEBUG || isBenchmark) console.log(average(times))
+ if (process.env.NANO_POW_DEBUG || isBenchmark) console.log(stats(times))
server.kill()
}
mkdir -p "$NANO_POW_LOGS";
if [ "$1" = '--server' ]; then
shift;
- node "$SCRIPT_DIR"/server.js --max-http-header-size=1024 --max-old-space-size=256 >> "$NANO_POW_LOGS"/nano-pow-server-$(date -I).log 2>&1 & echo "$!" > "$NANO_POW_HOME"/server.pid;
+ node --max-http-header-size=1024 --max-old-space-size=256 "$SCRIPT_DIR"/server.js >> "$NANO_POW_LOGS"/nano-pow-server-$(date -I).log 2>&1 & echo "$!" > "$NANO_POW_HOME"/server.pid;
sleep 0.1;
if [ "$(ps | grep $(cat $NANO_POW_HOME/server.pid))" = '' ]; then
cat $(ls -td "$NANO_POW_LOGS"/* | head -n1);
fi;
else
- node "$SCRIPT_DIR"/cli.js --max-old-space-size=256 "$@";
+ node --max-old-space-size=256 "$SCRIPT_DIR"/cli.js "$@";
fi;
//! SPDX-License-Identifier: GPL-3.0-or-later
import * as http from 'node:http'
+import { Serializable } from 'node:child_process'
import { hash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
-import { AddressInfo } from 'node:net'
import { homedir } from 'node:os'
import { join } from 'node:path'
-
import { launch } from 'puppeteer'
+import { WorkErrorResponse, WorkGenerateResponse, WorkValidateResponse } from '#types'
+import { Logger } from '#utils'
-import { isNotHex, log } from '../utils'
-import {
- NanoPowOptions,
- NanoPowServerConfig,
- WorkGenerateRequest,
- WorkGenerateResponse,
- WorkValidateRequest,
- WorkValidateResponse
-} from '#types'
+/**
+* Used to define NanoPow server configuration.
+*
+* @param {boolean} DEBUG - Enables additional log output
+* @param {number} EFFORT - Defines dispatch size per compute pass
+* @param {number} PORT - TCP port on which to listen for requests
+*/
+type NanoPowServerConfig = {
+ [key: string]: boolean | number
+ DEBUG: boolean
+ EFFORT: number
+ PORT: number
+}
process.title = 'NanoPow Server'
+const logger = new Logger()
const MAX_CONNECTIONS = 1024
const MAX_HEADER_COUNT = 32
-const MAX_IDLE_TIME = 5_000
+const MAX_IDLE_TIME = 30_000
const MAX_REQUEST_COUNT = 10
const MAX_REQUEST_SIZE = 256
const MAX_REQUEST_TIME = 60_000
let configFile = ''
try {
configFile = await readFile(join(homedir(), '.nano-pow', 'config'), 'utf-8')
- } catch {
- log('Config file not found')
- }
+ } catch { }
if (configFile.length > 0) {
for (const line of configFile.split('\n')) {
for (const [k, { r, v }] of Object.entries(configPatterns)) {
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
- log(`Config loaded ${JSON.stringify(CONFIG)}`)
+ logger.isEnabled = CONFIG.DEBUG
+ if (configFile.length === 0) logger.log('config file not found')
+ logger.log(`config set: ${JSON.stringify(CONFIG)}`)
}
await loadConfig()
// Initialize puppeteer
-log('Starting NanoPow work server')
+logger.log('starting work server')
const NanoPow = await readFile(new URL('../main.min.js', import.meta.url), 'utf-8')
+logger.log('launching puppeteer browser')
const browser = await launch({
handleSIGHUP: false,
handleSIGINT: false,
handleSIGTERM: false,
headless: true,
args: [
- '--headless=new',
'--disable-vulkan-surface',
'--enable-features=Vulkan,DefaultANGLEVulkan,VulkanFromANGLE',
'--enable-gpu',
'--enable-unsafe-webgpu'
]
})
+logger.log('creating puppeteer page')
const page = await browser.newPage()
-const src = `${NanoPow};window.NanoPow=NanoPowGpu;`
+const src = `${NanoPow};window.NanoPow=NanoPow;`
const enc = `sha256-${hash('sha256', src, 'base64')}`
const body = `
<!DOCTYPE html>
<script type="module">${src}</script>
`
+logger.log('setting puppeteer request interception')
await page.setRequestInterception(true)
page.on('request', req => {
if (req.isInterceptResolutionHandled()) return
req.continue()
}
})
-page.on('console', msg => log(msg.text()))
+page.on('console', msg => logger.log(msg.text()))
+logger.log('navigating to https://nanopow.invalid/ to be intercepted')
await page.goto('https://nanopow.invalid/')
-let NanoPowHandle = await page.waitForFunction(() => {
+let NanoPowHandle = await page.waitForFunction(async () => {
+ await window.navigator.gpu.requestAdapter()
return window.NanoPow
})
-log('Puppeteer initialized')
+logger.log('puppeteer initialized')
// Track requests by IP address, and let them fall off over time
const requests = new Map<string, { time: number, tokens: number }>()
}
}, MAX_REQUEST_TIME)
+async function work (data: unknown): Promise<string> {
+ let resBody = 'request failed'
+ if (data == null || typeof data !== 'object') {
+ logger.log('Failed to parse request')
+ throw new Error(resBody)
+ }
+ if (Object.getPrototypeOf(data) !== Object.prototype) {
+ logger.log('Data corrupted.')
+ throw new Error(resBody)
+ }
+ const dataParsed = data as { [key: string]: unknown }
+
+ let { action, hash, work, difficulty } = dataParsed
+ if (action !== 'work_generate' && action !== 'work_validate') {
+ logger.log('Action must be work_generate or work_validate.')
+ throw new Error(resBody)
+ }
+ resBody = `${action} failed`
+
+ if (typeof hash !== 'string' || !/[A-Fa-f\d]{64}/.test(hash)) {
+ logger.log('Hash must be hex char(64)')
+ throw new Error(resBody)
+ }
+ if (difficulty !== undefined
+ && (typeof difficulty !== 'string'
+ || !/^[A-Fa-f\d]{16}$/.test(difficulty))
+ ) {
+ logger.log('Difficulty must be hex char(16).')
+ throw new Error(resBody)
+ }
+ if (action === 'work_validate') {
+ if (typeof work !== 'string' || !/^[A-Fa-f\d]{16}$/.test(work)) {
+ logger.log('Work must be hex char(16).')
+ throw new Error(resBody)
+ }
+ } else {
+ function f (n: any): asserts n is string { if (false) throw new Error() }
+ f(work)
+ }
+
+ const options = {
+ api: 'webgpu',
+ debug: CONFIG.DEBUG,
+ effort: CONFIG.EFFORT,
+ difficulty
+ }
+ try {
+ const result = (action === 'work_validate')
+ ? await page.evaluate((n, w, h, o): Promise<WorkValidateResponse | WorkErrorResponse> => {
+ if (n == null) throw new Error('NanoPow not found')
+ return n.work_validate(w, h, o)
+ }, NanoPowHandle, work, hash, options)
+ : await page.evaluate((n, h, o): Promise<WorkGenerateResponse | WorkErrorResponse> => {
+ if (n == null) throw new Error('NanoPow not found')
+ return n.work_generate(h, o)
+ }, NanoPowHandle, hash, options)
+ resBody = JSON.stringify(result)
+ return resBody
+ } catch (err) {
+ logger.log(err)
+ throw new Error(resBody)
+ }
+}
+
function get (res: http.ServerResponse): void {
res
.writeHead(200, { 'Content-Type': 'text/plain' })
Validate WORK previously calculated for a BLOCKHASH with an optional DIFFICULTY:
curl -d '{ action: "work_validate", work: WORK, hash: BLOCKHASH, difficulty?: DIFFICULTY }'
-BLOCKHASH is a 64-character hexadecimal string.
-WORK is 16-character hexadecimal string.
-DIFFICULTY is a 16-character hexadecimal string (default: FFFFFFF800000000)
+BLOCKHASH 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>
let resBody = 'request failed'
try {
- const reqBody: WorkGenerateRequest | WorkValidateRequest = JSON.parse(Buffer.concat(reqData).toString())
- if (Object.getPrototypeOf(reqBody) !== Object.prototype) {
- throw new Error('Data corrupted.')
- }
-
- const { action, hash, work, difficulty } = reqBody
- if (action !== 'work_generate' && action !== 'work_validate') {
- throw new Error('Action must be work_generate or work_validate.')
- }
- resBody = `${action} failed`
-
- if (isNotHex(hash, 64)) {
- throw new Error('Hash must be a 64-character hex string.')
- }
- if (difficulty && isNotHex(difficulty, 1, 16)) {
- throw new Error('Difficulty must be a hex string between 0-FFFFFFFFFFFFFFFF.')
- }
- if (action === 'work_validate' && isNotHex(work, 16)) {
- throw new Error('Work must be a 16-character hex string.')
- }
-
- const options: NanoPowOptions = {
- debug: CONFIG.DEBUG,
- effort: CONFIG.EFFORT,
- difficulty
- }
- const result = (action === 'work_generate')
- ? await page.evaluate((n, h, o): Promise<WorkGenerateResponse> => {
- if (n == null) throw new Error('NanoPow not found')
- return n.work_generate(h, o)
- }, NanoPowHandle, hash, options)
- : await page.evaluate((n, w, h, o): Promise<WorkValidateResponse> => {
- if (n == null) throw new Error('NanoPow not found')
- return n.work_validate(w, h, o)
- }, NanoPowHandle, work, hash, options)
- resBody = JSON.stringify(result)
+ resBody = await work(JSON.parse(Buffer.concat(reqData).toString()))
resStatusCode = 200
- } catch (err) {
- log(err)
+ } catch (err: any) {
+ resBody = err.message
resStatusCode = 400
} finally {
res.writeHead(resStatusCode, resHeaders).end(resBody)
}
const client = requests.get(ip)
if (client && client.tokens-- <= 0) {
- log(`==== Potential Abuse: ${ip} ====`)
+ logger.log(`==== Potential Abuse: ${ip} ====`)
return true
}
if (Date.now() - MAX_REQUEST_TIME > (client?.time ?? 0)) {
* to the parent process with the port number assigned by the operating system.
*/
function listen (): void {
- server.listen(CONFIG.PORT, '127.0.0.1', () => {
- const { port } = server.address() as AddressInfo
- CONFIG.PORT = port
- log(`Server listening on port ${port}`)
- process.send?.({ type: 'listening', message: port })
- })
+ if (process.channel) {
+ process.on('message', async (msg: Serializable) => {
+ 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: any) {
+ process.send?.({ type: 'ipc', data: err.stack })
+ }
+ }
+ }
+ })
+ 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) {
+ logger.log(`server closed`)
+ } else if (typeof address === 'string') {
+ logger.log(`server listening on ${address}`)
+ process.send?.({ type: 'listening', data: address })
+ } else {
+ CONFIG.PORT = address.port
+ logger.log(`server listening on port ${address.port}`)
+ process.send?.({ type: 'listening', data: address.port })
+ }
+ })
+ }
}
/**
})
server.on('error', serverErr => {
- log('Server error', serverErr)
+ logger.log('server error', serverErr)
try {
shutdown()
} catch (shutdownErr) {
- log('Failed to shut down', shutdownErr)
+ logger.log('server failed to shut down', shutdownErr)
process.exit(1)
}
})
* does not respond within 10 seconds.
*/
function shutdown (): void {
- log('Shutdown signal received')
+ logger.log('shutdown signal received')
const kill = setTimeout((): never => {
- log('Server unresponsive, forcefully stopped')
+ logger.log('server unresponsive, forcefully stopped')
process.exit(1)
}, 10_000)
server.close(async (): Promise<never> => {
await browser.close()
clearTimeout(kill)
- log('Server stopped')
+ logger.log('server stopped')
process.exit(0)
})
}
process.on('SIGTERM', shutdown)
process.on('SIGHUP', async () => {
- log('Reloading configuration')
+ logger.log('reloading configuration')
server.close(async () => {
await loadConfig()
await page.reload()
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { ApiSupportedTypes, NanoPowOptions } from '#types'
+import { ApiSupport, bigintFrom, bigintToHex, SEND } from '#utils'
+
+class NanoPowConfigConstructor implements NanoPowOptions {
+ static #isInternal: boolean = false
+ api: ApiSupportedTypes
+ debug: boolean
+ difficulty: bigint
+ effort: number
+
+ toJSON () {
+ return {
+ api: this.api,
+ debug: this.debug,
+ difficulty: bigintToHex(this.difficulty, 16),
+ effort: this.effort
+ }
+ }
+
+ constructor (api: ApiSupportedTypes, debug: boolean, difficulty: bigint, effort: number) {
+ if (!NanoPowConfigConstructor.#isInternal) {
+ throw new TypeError(`NanoPowConfig cannot be constructed with 'new'.`)
+ }
+ this.api = api
+ this.debug = debug
+ this.difficulty = difficulty
+ this.effort = effort
+ NanoPowConfigConstructor.#isInternal = false
+ }
+
+ static async create (options: { api: unknown, debug: unknown, difficulty: unknown, effort: unknown } | unknown): Promise<NanoPowConfigConstructor> {
+ const input = options as Record<keyof NanoPowOptions, unknown>
+
+ const api = await this.#getValidApi(input)
+ const debug = this.#getValidDebug(input)
+ const difficulty = this.#getValidDifficulty(input)
+ const effort = this.#getValidEffort(input)
+
+ this.#isInternal = true
+ const config = new this(api, debug, difficulty, effort)
+ return config
+ }
+
+ // Check platform support for default API setting
+ static async #getDefaultApi (): Promise<ApiSupportedTypes> {
+ if (await ApiSupport.webgpu.isSupported) return 'webgpu'
+ if (await ApiSupport.webgl.isSupported) return 'webgl'
+ if (await ApiSupport.wasm.isSupported) return 'wasm'
+ return 'cpu'
+ }
+
+ // Assign API if valid value passed
+ static async #getValidApi (input: Record<string, unknown>): Promise<ApiSupportedTypes> {
+ if (input.api != null) {
+ if (typeof input.api === 'string') {
+ try {
+ input.api = input.api.toLowerCase()
+ } catch {
+ input.api = null
+ }
+ }
+ if (input.api !== 'cpu'
+ && input.api !== 'wasm'
+ && input.api !== 'webgl'
+ && input.api !== 'webgpu'
+ ) {
+ throw new Error(`Invalid API ${input.api}`)
+ }
+ if (!(ApiSupport[input.api].isSupported)) {
+ throw new Error(`${input.api} is not supported`)
+ }
+ return input.api
+ }
+ return this.#getDefaultApi()
+ }
+
+ // Assign debug if valid value passed
+ static #getValidDebug (input: Record<string, unknown>): boolean {
+ if (input.debug != null) {
+ if (typeof input.debug === 'bigint' || typeof input.debug === 'number') {
+ input.debug = input.debug.toString()
+ }
+ if (typeof input.debug === 'string') {
+ input.debug = ['1', 'true', 'y', 'yes'].includes(input.debug.toLowerCase())
+ }
+ if (typeof input.debug !== 'boolean') {
+ throw new Error(`Invalid debug ${input.debug}`)
+ }
+ return input.debug
+ }
+ return false
+ }
+
+ // Assign difficulty if valid value passed
+ static #getValidDifficulty (input: Record<string, unknown>): bigint {
+ if (input.difficulty != null) {
+ if (typeof input.difficulty === 'string') {
+ try {
+ input.difficulty = bigintFrom(input.difficulty, 'hex')
+ } catch { }
+ }
+ if (typeof input.difficulty !== 'bigint') {
+ throw new Error(`Invalid difficulty (${typeof input.difficulty})${input.difficulty}`)
+ }
+ if (input.difficulty < 0x0n || input.difficulty > SEND) {
+ throw new Error(`Invalid difficulty ${bigintToHex(input.difficulty, 16)}`)
+ }
+ return input.difficulty
+ }
+ return SEND
+ }
+
+ // Assign effort if valid value passed
+ static #getValidEffort (input: Record<string, unknown>): number {
+ if (input.effort != null) {
+ if (typeof input.effort !== 'number') {
+ throw new Error(`Invalid effort (${typeof input.effort})${input.effort}`)
+ }
+ if (input.effort < 0x1 || input.effort > 0x20) {
+ throw new Error(`Invalid effort ${input.effort}`)
+ }
+ return input.effort
+ }
+ return 0x4
+ }
+}
+
+/**
+* Validated NanoPowOptions object used to guarantee values and types. Attempting
+* to call using `new` with throw a TypeError.
+*
+* @param {string} api - Specifies how work is generated. Default: best available
+* @param {boolean} debug - Enables additional debug logging to the console. Default: false
+* @param {bigint} difficulty - Minimum value result of `BLAKE2b(nonce||blockhash)`. Default: 0xFFFFFFF800000000
+* @param {number} effort - GPU load when generating work. Larger values are not necessarily better since they can quickly overwhelm the GPU. Default: 0x4
+*/
+export const NanoPowConfig = (options: any): Promise<NanoPowConfigConstructor> => {
+ try {
+ return NanoPowConfigConstructor.create(options)
+ } catch (err) {
+ throw new Error('Error constructing NanoPowConfig', { cause: err })
+ }
+}
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { NanoPowValidate } from '#lib/validate'
+import { WorkGenerateResponse } from '#types'
+import { bigintRandom } from '#utils'
+
+export function generate (hash: bigint, difficulty: bigint, debug: boolean): WorkGenerateResponse {
+ let result
+ do {
+ result = NanoPowValidate(bigintRandom(), hash, difficulty, debug)
+ } while (result.valid !== '1')
+ return {
+ hash: result.hash,
+ work: result.work,
+ difficulty: result.difficulty
+ }
+}
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+export { generate as NanoPowCpu } from '#lib/generate/cpu'
+export { generate as NanoPowWasm } from '#lib/generate/wasm'
+export { generate as NanoPowWebgl } from '#lib/generate/webgl'
+export { generate as NanoPowWebgpu } from '#lib/generate/webgpu'
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+const blake2b_sigma: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15)[][] = [
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
+ [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
+ [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
+ [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
+ [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
+ [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
+ [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
+ [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
+ [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
+ [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
+ [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3]
+]
+
+/**
+* Initialization vector defined by BLAKE2.
+*/
+const blake2b_iv = [
+ '0x6a09e667f3bcc908',
+ '0xbb67ae8584caa73b',
+ '0x3c6ef372fe94f82b',
+ '0xa54ff53a5f1d36f1',
+ '0x510e527fade682d1',
+ '0x9b05688c2b3e6c1f',
+ '0x1f83d9abfb41bd6b',
+ '0x5be0cd19137e2179'
+]
+
+/**
+* Parameter block as defined in BLAKE2 section 2.8 and configured as follows:
+* maximal depth = 1, fanout = 1, digest byte length = 8
+*/
+const blake2b_param = `<u64>${blake2b_iv[0]} ^ <u64>0x01010008`
+
+/**
+* Message input length which is always 40 for Nano.
+* 8 nonce bytes + 32 block hash bytes
+*/
+const blake2b_inlen = `${blake2b_iv[4]} ^ <u64>0x28`
+
+/**
+* Finalization flag as defined in BLAKE2 section 2.4 and set to ~0 since this is
+* the final (and only) message block being hashed.
+*/
+const blake2b_final = `~${blake2b_iv[6]}`
+
+function G (
+ a: 0 | 1 | 2 | 3,
+ b: 4 | 5 | 6 | 7,
+ c: 8 | 9 | 10 | 11,
+ d: 12 | 13 | 14 | 15,
+ x: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15,
+ y: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15
+): string {
+ return `
+ v${a} = unchecked(v128.add<u64>(v${a}, v${b}))
+ v${a} = unchecked(v128.add<u64>(v${a}, m${x}))
+ v${d} = v128.xor(v${d}, v${a})
+ v${d} = v128.shuffle<u8>(v${d}, v${d}, 4, 5, 6, 7, 0, 1, 2, 3, 12, 13, 14, 15, 8, 9, 10, 11)
+ v${c} = unchecked(v128.add<u64>(v${c}, v${d}))
+ v${b} = v128.xor(v${b}, v${c})
+ v${b} = v128.shuffle<u8>(v${b}, v${b}, 3, 4, 5, 6, 7, 0, 1, 2, 11, 12, 13, 14, 15, 8, 9, 10)
+ v${a} = unchecked(v128.add<u64>(v${a}, v${b}))
+ v${a} = unchecked(v128.add<u64>(v${a}, m${y}))
+ v${d} = v128.xor(v${d}, v${a})
+ v${d} = v128.shuffle<u8>(v${d}, v${d}, 2, 3, 4, 5, 6, 7, 0, 1, 10, 11, 12, 13, 14, 15, 8, 9)
+ v${c} = unchecked(v128.add<u64>(v${c}, v${d}))
+ v${b} = v128.xor(v${b}, v${c})
+ v${b} = v128.or(v128.shr<u64>(v${b}, 63), v128.shl<u64>(v${b}, 1))
+ `
+}
+
+function ROUND (i: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11)): string {
+ return `
+ // ROUND ${i}
+ ${G(0, 4, 8, 12, blake2b_sigma[i][0], blake2b_sigma[i][1])}
+ ${G(1, 5, 9, 13, blake2b_sigma[i][2], blake2b_sigma[i][3])}
+ ${G(2, 6, 10, 14, blake2b_sigma[i][4], blake2b_sigma[i][5])}
+ ${G(3, 7, 11, 15, blake2b_sigma[i][6], blake2b_sigma[i][7])}
+ ${G(0, 5, 10, 15, blake2b_sigma[i][8], blake2b_sigma[i][9])}
+ ${G(1, 6, 11, 12, blake2b_sigma[i][10], blake2b_sigma[i][11])}
+ ${G(2, 7, 8, 13, blake2b_sigma[i][12], blake2b_sigma[i][13])}
+ ${G(3, 4, 9, 14, blake2b_sigma[i][14], blake2b_sigma[i][15])}
+ `
+}
+
+function SETUP (): string {
+ return `
+ // input parameter configuration
+ // v0: depth=1; fanout=1; outlen=8
+ // v12: inlen
+ // v14: final block flag
+ let v0 = v128.splat<u64>(${blake2b_param})
+ let v1 = v128.splat<u64>(${blake2b_iv[1]})
+ let v2 = v128.splat<u64>(${blake2b_iv[2]})
+ let v3 = v128.splat<u64>(${blake2b_iv[3]})
+ let v4 = v128.splat<u64>(${blake2b_iv[4]})
+ let v5 = v128.splat<u64>(${blake2b_iv[5]})
+ let v6 = v128.splat<u64>(${blake2b_iv[6]})
+ let v7 = v128.splat<u64>(${blake2b_iv[7]})
+ let v8 = v128.splat<u64>(${blake2b_iv[0]})
+ let v9 = v128.splat<u64>(${blake2b_iv[1]})
+ let v10 = v128.splat<u64>(${blake2b_iv[2]})
+ let v11 = v128.splat<u64>(${blake2b_iv[3]})
+ let v12 = v128.splat<u64>(${blake2b_inlen})
+ let v13 = v128.splat<u64>(${blake2b_iv[5]})
+ let v14 = v128.splat<u64>(${blake2b_final})
+ let v15 = v128.splat<u64>(${blake2b_iv[7]})
+ `
+}
+
+function hash (): string {
+ return `
+ ${SETUP()}
+ ${ROUND(0)}
+ ${ROUND(1)}
+ ${ROUND(2)}
+ ${ROUND(3)}
+ ${ROUND(4)}
+ ${ROUND(5)}
+ ${ROUND(6)}
+ ${ROUND(7)}
+ ${ROUND(8)}
+ ${ROUND(9)}
+ ${ROUND(10)}
+ ${ROUND(11)}
+ `
+}
+
+console.log(`export function main (seed: u64, h0: u64, h1: u64, h2: u64, h3: u64, difficulty: u64): u64 {
+ let m0 = v128.splat<u64>(seed)
+ const m1 = v128.splat<u64>(h0)
+ const m2 = v128.splat<u64>(h1)
+ const m3 = v128.splat<u64>(h2)
+ const m4 = v128.splat<u64>(h3)
+ const m5 = v128.splat<u64>(0)
+ const m6 = v128.splat<u64>(0)
+ const m7 = v128.splat<u64>(0)
+ const m8 = v128.splat<u64>(0)
+ const m9 = v128.splat<u64>(0)
+ const m10 = v128.splat<u64>(0)
+ const m11 = v128.splat<u64>(0)
+ const m12 = v128.splat<u64>(0)
+ const m13 = v128.splat<u64>(0)
+ const m14 = v128.splat<u64>(0)
+ const m15 = v128.splat<u64>(0)
+ const finalizer = v128.splat<u64>(${blake2b_param})
+
+ let r0: u64 = 0
+ let r1: u64 = 0
+ let result = v128.splat<u64>(0)
+ const iterations: u64 = 1 << 24
+
+ for (let i: u64 = 0; i < iterations; i++) {
+ m0 = i64x2(unchecked(seed + i), unchecked(seed + i + 1))
+ i += 2
+ ${hash()}
+ result = v128.xor(finalizer, v128.xor(v0, v8))
+ r0 = v128.extract_lane<u64>(result, 0)
+ r1 = v128.extract_lane<u64>(result, 1)
+ if (r0 >= difficulty || r1 >= difficulty) break
+ }
+
+ if (r0 < difficulty && r1 < difficulty) {
+ return 1/0
+ }
+ return select(v128.extract_lane<u64>(m0, 0), v128.extract_lane<u64>(m0, 1), r0 >= difficulty)
+}
+`)
--- /dev/null
+{
+ "extends": "assemblyscript/std/assembly.json",
+ "include": [
+ "./*.ts"
+ ],
+ "compilerOptions": {
+ "outDir": "./build"
+ }
+}
--- /dev/null
+SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+SPDX-License-Identifier: GPL-3.0-or-later
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { NanoPowValidate } from '#lib/validate'
+import { NanoPowWasmWorker } from './worker.js'
+import { WorkGenerateResponse } from '#types'
+import { bigintRandom, bigintToHex, Logger } from '#utils'
+
+const logger = new Logger()
+
+// Initialize CPU
+let isReady: boolean = false
+let data: { [key: string]: string } = {}
+let v: BigUint64Array = new BigUint64Array(16)
+let m: BigUint64Array = new BigUint64Array(16)
+let workers: Worker[] = []
+let url: string
+
+function setup (): void {
+ try {
+ url = URL.createObjectURL(new Blob([NanoPowWasmWorker], { type: 'text/javascript' }))
+ workers = []
+ logger.log(`NanoPow CPU initialized.`)
+ isReady = true
+ } catch (err) {
+ isReady = false
+ throw new Error('NanoPow CPU initialization failed.', { cause: err })
+ }
+}
+
+function reset (): void {
+ console.warn(`NanoPow CPU encountered an error. Reinitializing...`)
+ isReady = false
+ for (const w of workers) w.terminate()
+ workers = []
+ m?.fill(0n)
+ v?.fill(0n)
+}
+
+async function init (hash: bigint, difficulty: bigint, effort: number): Promise<void> {
+ data.hash = bigintToHex(hash, 64)
+ data.difficulty = bigintToHex(difficulty, 16)
+
+ for (let i = workers.length; i < effort; i++) {
+ workers.push(new Worker(url, { type: 'module' }))
+ }
+ while (workers.length - effort > 0) {
+ workers.pop()?.terminate()
+ }
+ try {
+ await workersStarted()
+ logger.log('Workers ready')
+ } catch (err: any) {
+ logger.log(err.message)
+ throw new Error('Error loading workers')
+ }
+}
+
+async function workersStarted () {
+ return new Promise(async (ready, fail): Promise<void> => {
+ const resolutions = []
+ for (const w of workers) {
+ resolutions.push(new Promise((resolve, reject): void => {
+ w.onmessage = msg => msg.data === 'started' ? resolve(msg.data) : reject(msg.data)
+ w.onerror = err => reject(err.message)
+ w.postMessage('start')
+ }))
+ }
+ Promise.all(resolutions).then(ready).catch(fail)
+ })
+}
+
+async function dispatch (): Promise<bigint> {
+ return new Promise(resolve => {
+ const attempts = []
+ for (let i = 0; i < workers.length; i++) {
+ data.seed = bigintToHex((bigintRandom() & ~((1n << 24n) - 1n)), 16)
+ attempts.push(new Promise((found, err) => {
+ const w = workers[i]
+ w.onerror = err
+ w.onmessage = (msg) => {
+ const result = msg.data
+ logger.log(`received result from worker ${i}`)
+ found(result)
+ }
+ logger.log(`sending data to worker ${i}`)
+ w.postMessage(JSON.stringify(data))
+ }))
+ }
+ Promise.all(attempts).then(results => {
+ const result = results.find(r => typeof r === 'bigint')
+ result ? resolve(result) : resolve(dispatch())
+ })
+ })
+}
+
+async function workersStopped (): Promise<boolean> {
+ return new Promise(stopped => {
+ try {
+ const attempts = []
+ for (let i = 0; i < workers.length; i++) {
+ attempts.push(new Promise((resolve, reject) => {
+ const w = workers[i]
+ w.onerror = reject
+ w.onmessage = (msg) => {
+ const result = msg.data
+ if (result === 'stopped') {
+ resolve(result)
+ } else {
+ reject(i)
+ }
+ }
+ w.postMessage('stop')
+ }))
+ }
+ Promise.allSettled(attempts).then(results => {
+ for (const result of results) {
+ if (result.status === 'rejected') {
+ const i = result.reason
+ workers[i].terminate()
+ workers.splice(i)
+ }
+ }
+ stopped(true)
+ })
+ } catch {
+ stopped(false)
+ }
+ })
+}
+
+/**
+* Nano proof-of-work using WebAssembly.
+*/
+export async function generate (hash: bigint, difficulty: bigint, effort: number, debug: boolean): Promise<WorkGenerateResponse> {
+ logger.isEnabled = debug
+ logger.groupStart('NanoPow WASM work_generate')
+ logger.log('generating')
+ if (isReady === false) setup()
+ await init(hash, difficulty, effort)
+
+ let work = 0n
+ let result = ''
+ try {
+ work = await dispatch()
+ result = (await NanoPowValidate(work, hash, difficulty, debug)).difficulty
+ } catch (err) {
+ logger.log(err)
+ } finally {
+ const isStopped = await workersStopped()
+ if (isStopped) {
+ logger.log('workers stopped')
+ } else {
+ reset()
+ logger.log('workers reset')
+ }
+ logger.groupEnd('NanoPow WASM work_generate')
+ }
+
+ return {
+ hash: bigintToHex(hash, 64),
+ work: bigintToHex(work, 16),
+ difficulty: result
+ }
+}
+
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+//@ts-expect-error
+import compute from './asm/build/compute.wasm'
+type Main = (w: bigint, h0: bigint, h1: bigint, h2: bigint, h3: bigint, d: bigint) => any
+
+const worker = async (compute: number[]): Promise<void> => {
+ let isReady = false
+ let wasm
+ let module
+ let instance
+ let main: Main
+
+ async function setup (): Promise<void> {
+ try {
+ wasm = Uint8Array.from(compute)
+ module = await WebAssembly.compile(wasm)
+ instance = await WebAssembly.instantiate(module, {
+ env: {
+ abort: (message: any, fileName: any, lineNumber: any, columnNumber: any) => {
+ console.error('Wasm abort:', message, fileName, lineNumber, columnNumber)
+ throw new Error(`Wasm abort: ${message}`)
+ },
+ memory: new WebAssembly.Memory({ initial: 256, maximum: 1024 })
+ }
+ })
+ main = instance.exports.main as Main
+ isReady = true
+ } catch (err) {
+ throw new Error('Error instantiating WebAssembly', { cause: err })
+ }
+ }
+
+ async function handleMessage (msg: any): Promise<void> {
+ let result: any = null
+ try {
+ if (!isReady) await setup()
+ const hashArray = new BigUint64Array(4)
+ const hashView = new DataView(hashArray.buffer)
+
+ if (msg.data === 'start') {
+ result = 'started'
+ } else if (msg.data === 'stop') {
+ removeEventListener('message', handleMessage)
+ result = 'stopped'
+ } else {
+ const data = JSON.parse(msg.data)
+ const seed = BigInt(`0x${data.seed}`)
+ const difficulty = BigInt(`0x${data.difficulty}`)
+ for (let i = 0; i < data.hash.length; i += 16) {
+ const u64 = data.hash.slice(i, i + 16)
+ hashView.setBigUint64(i / 2, BigInt(`0x${u64}`))
+ }
+ const work = main(seed, hashArray[0], hashArray[1], hashArray[2], hashArray[3], difficulty)
+ if (typeof work !== 'bigint') {
+ throw new TypeError('Invalid work from WASM')
+ }
+ const workArray = new BigUint64Array(1)
+ const workView = new DataView(workArray.buffer)
+ workView.setBigUint64(0, work, true)
+ result = workArray[0]
+ }
+ } catch (err: unknown) {
+ if (typeof err === 'object' && err != null) {
+ const e = err as { [k: string]: unknown }
+ if (e.message !== 'divide by zero') {
+ result = e.message
+ }
+ } else {
+ result = JSON.stringify(err)
+ }
+ } finally {
+ postMessage(result)
+ }
+ }
+
+ addEventListener('message', handleMessage)
+}
+
+export const NanoPowWasmWorker = `
+ ;await (${worker})([${compute}])
+`
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-FileContributor: Ben Green <ben@latenightsketches.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later AND MIT
+
+import { downsampleSource, drawSource, quadSource } from './shaders'
+import { WorkGenerateResponse } from '#types'
+import { bigintAsUintNArray, bigintRandom, bigintToHex, Logger } from '#utils'
+
+/**
+* Used to create WebGL framebuffer objects.
+*
+* @param {WebGLTexture} texture - Defines storage size
+* @param {WebGLFramebuffer} framebuffer - Holds texture data
+* @param {size} size - 2D lengths of texture
+*/
+type FBO = {
+ texture: WebGLTexture
+ framebuffer: WebGLFramebuffer
+ size: {
+ x: number
+ y: number
+ }
+}
+
+const logger = new Logger()
+
+// Vertex positions for fullscreen quad.
+const positions: Float32Array = new Float32Array([
+ -1, -1, 1, -1, 1, 1, -1, 1
+])
+// Host-side buffers
+const inputArray: Uint32Array = new Uint32Array(36)
+const inputView: DataView = new DataView(inputArray.buffer)
+const inputHashView: DataView = new DataView(inputArray.buffer, 0, 128)
+const inputDifficultyView: DataView = new DataView(inputArray.buffer, 128, 8)
+const inputSeedView: DataView = new DataView(inputArray.buffer, 136, 8)
+const resultArray: BigUint64Array = new BigUint64Array(2)
+const resultView: DataView = new DataView(resultArray.buffer)
+
+// GPU variables
+let canvas: OffscreenCanvas
+let gl: WebGL2RenderingContext
+let drawProgram: WebGLProgram | null
+let downsampleProgram: WebGLProgram | null
+let vertexShader: WebGLShader | null
+let drawShader: WebGLShader | null
+let downsampleShader: WebGLShader | null
+let positionBuffer: WebGLBuffer | null
+let queries: WebGLQuery[]
+let drawFbos: FBO[]
+let downsampleFbos: FBO[]
+let downsampleSrcLocation: WebGLUniformLocation | null
+let inputBuffer: WebGLBuffer | null
+let pixels: Uint32Array
+
+// Process variables
+let isContextLost = 0
+let isReady: boolean = false
+let drawEffort: number = 0x10
+let raf: number = 0
+
+// Create persistent resizable canvas and get WebGL2 context
+function createCanvas (size: number): void {
+ logger.groupStart('NanoPow WebGL createCanvas')
+ if (canvas == null) {
+ canvas = new OffscreenCanvas(0, 0)
+ canvas.addEventListener('webglcontextlost', ev => {
+ // Set up 10s timeout to prevent long-running restoration
+ isContextLost = window.setTimeout(() => {
+ throw new Error('NanoPow could not restore WebGL context.')
+ }, 10_000)
+ ev.preventDefault()
+ logger.log('NanoPow WebGL createCanvas', 'WebGL context lost. Waiting for it to be restored...')
+ })
+ canvas.addEventListener('webglcontextrestored', ev => {
+ window.clearTimeout(isContextLost)
+ isContextLost = 0
+ logger.log('NanoPow WebGL createCanvas', 'WebGL context restored. Reinitializing...')
+ })
+ }
+
+ const context: WebGL2RenderingContext | null = canvas.getContext('webgl2', {
+ alpha: false,
+ antialias: false,
+ depth: false,
+ failIfMajorPerformanceCaveat: true,
+ powerPreference: 'default',
+ premultipliedAlpha: false,
+ stencil: false
+ })
+ if (context == null) throw new Error('WebGL 2 is required')
+ gl = context
+
+ logger.log('NanoPow WebGL createCanvas', 'requested size', size)
+ logger.log('NanoPow WebGL createCanvas', 'MAX_VIEWPORT_DIMS', ...gl.getParameter(gl.MAX_VIEWPORT_DIMS))
+ size = Math.min(size, ...gl.getParameter(gl.MAX_VIEWPORT_DIMS))
+ size = Math.floor(size / 0x100) * 0x100
+ canvas.height = canvas.width = size
+ logger.log('NanoPow WebGL createCanvas', 'canvas size', canvas.height, canvas.width)
+ logger.log('NanoPow WebGL createCanvas', 'drawingBuffer size', gl.drawingBufferHeight, gl.drawingBufferWidth)
+ if (canvas.height !== gl.drawingBufferHeight
+ || canvas.width !== gl.drawingBufferWidth
+ ) {
+ size = Math.floor(Math.min(gl.drawingBufferHeight, gl.drawingBufferWidth) / 0x100) * 0x100
+ canvas.height = canvas.width = size
+ }
+ logger.log('NanoPow WebGL createCanvas', 'final size', size)
+ logger.groupEnd('NanoPow WebGL createCanvas')
+}
+
+function compile (): void {
+ try {
+ // Create drawing program
+ drawProgram = gl.createProgram()
+ if (drawProgram == null) throw new Error('Failed to create shader program')
+
+ vertexShader = gl.createShader(gl.VERTEX_SHADER)
+ if (vertexShader == null) throw new Error('Failed to create vertex shader')
+ gl.shaderSource(vertexShader, quadSource)
+ gl.compileShader(vertexShader)
+ if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS))
+ throw new Error(gl.getShaderInfoLog(vertexShader) ?? `Failed to compile vertex shader`)
+
+ drawShader = gl.createShader(gl.FRAGMENT_SHADER)
+ if (drawShader == null) throw new Error('Failed to create fragment shader')
+ gl.shaderSource(drawShader, drawSource)
+ gl.compileShader(drawShader)
+ if (!gl.getShaderParameter(drawShader, gl.COMPILE_STATUS))
+ throw new Error(gl.getShaderInfoLog(drawShader) ?? `Failed to compile fragment shader`)
+
+ gl.attachShader(drawProgram, vertexShader)
+ gl.attachShader(drawProgram, drawShader)
+ gl.linkProgram(drawProgram)
+ if (!gl.getProgramParameter(drawProgram, gl.LINK_STATUS))
+ throw new Error(gl.getProgramInfoLog(drawProgram) ?? `Failed to link program`)
+
+ // Create downsampling program
+ downsampleProgram = gl.createProgram()
+ if (downsampleProgram == null) throw new Error('Failed to create downsample program')
+
+ downsampleShader = gl.createShader(gl.FRAGMENT_SHADER)
+ if (downsampleShader == null) throw new Error('Failed to create downsample shader')
+ gl.shaderSource(downsampleShader, downsampleSource)
+ gl.compileShader(downsampleShader)
+ if (!gl.getShaderParameter(downsampleShader, gl.COMPILE_STATUS))
+ throw new Error(gl.getShaderInfoLog(downsampleShader) ?? `Failed to compile downsample shader`)
+
+ gl.attachShader(downsampleProgram, vertexShader)
+ gl.attachShader(downsampleProgram, downsampleShader)
+ gl.linkProgram(downsampleProgram)
+ if (!gl.getProgramParameter(downsampleProgram, gl.LINK_STATUS))
+ throw new Error(gl.getProgramInfoLog(downsampleProgram) ?? `Failed to link program`)
+
+ // Construct fullscreen quad for rendering
+ gl.useProgram(drawProgram)
+ const triangleArray = gl.createVertexArray()
+ gl.bindVertexArray(triangleArray)
+
+ positionBuffer = gl.createBuffer()
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
+ gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW)
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0)
+ gl.enableVertexAttribArray(0)
+ gl.bindBuffer(gl.ARRAY_BUFFER, null)
+
+ // Create textures, framebuffers, and queries for drawing
+ drawFbos = []
+ queries = []
+ for (let i = 0; i < 4; i++) {
+ const texture = gl.createTexture()
+ gl.bindTexture(gl.TEXTURE_2D, texture)
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32UI, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, gl.RGBA_INTEGER, gl.UNSIGNED_INT, null)
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
+
+ const framebuffer = gl.createFramebuffer()
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)
+ if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE)
+ throw new Error(`Failed to create drawing framebuffer`)
+ drawFbos.push({ texture, framebuffer, size: { x: gl.drawingBufferWidth, y: gl.drawingBufferHeight } })
+ queries.push(gl.createQuery())
+ }
+
+ // Create textures, framebuffers, and uniform location for downsampling
+ downsampleFbos = []
+ for (let i = 1; i <= 8; i++) {
+ const width = gl.drawingBufferWidth / (2 ** i)
+ const height = gl.drawingBufferHeight / (2 ** i)
+
+ const texture = gl.createTexture()
+ gl.bindTexture(gl.TEXTURE_2D, texture)
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32UI, width, height, 0, gl.RGBA_INTEGER, gl.UNSIGNED_INT, null)
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
+
+ const framebuffer = gl.createFramebuffer()
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)
+ if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE)
+ throw new Error(`Failed to create downsampling framebuffer ${i}`)
+ downsampleFbos.push({ texture, framebuffer, size: { x: width, y: height } })
+ }
+ downsampleSrcLocation = gl.getUniformLocation(downsampleProgram, 'src')
+ gl.bindTexture(gl.TEXTURE_2D, null)
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null)
+
+ const finalFbo = downsampleFbos.slice(-1)[0]
+
+ // Input buffers must align to multiple of 16
+ inputBuffer = gl.createBuffer()
+ gl.bindBuffer(gl.UNIFORM_BUFFER, inputBuffer)
+ gl.bufferData(gl.UNIFORM_BUFFER, 160, gl.DYNAMIC_DRAW)
+ gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, inputBuffer)
+ gl.uniformBlockBinding(drawProgram, gl.getUniformBlockIndex(drawProgram, 'INPUT'), 0)
+ gl.bindBuffer(gl.UNIFORM_BUFFER, null)
+
+ // Finalize configuration
+ pixels = new Uint32Array(finalFbo.size.x * finalFbo.size.y * 4)
+ isReady = true
+ logger.log(`NanoPow WebGL initialized at ${gl.drawingBufferWidth}x${gl.drawingBufferHeight}. Maximum nonces checked per frame: ${gl.drawingBufferWidth * gl.drawingBufferHeight}`)
+ } catch (err) {
+ throw new Error('WebGL compilation failed.', { cause: err })
+ }
+}
+
+/**
+* Constructs canvas & WebGL2 context, compiles shaders, and initializes buffers.
+*/
+function setup (effort: number): void {
+ try {
+ reset()
+ drawEffort = effort
+ createCanvas(drawEffort * 0x100)
+ compile()
+ } catch (err) {
+ reset()
+ throw new Error('WebGL setup failed.', { cause: err })
+ }
+}
+
+/**
+* On effort change or WebGL context loss, clears all program variables.
+*/
+function reset (): void {
+ isReady = false
+ cancelAnimationFrame(raf)
+ raf = 0
+ gl?.deleteBuffer(inputBuffer)
+ inputBuffer = null
+ for (const fbo of downsampleFbos ?? []) {
+ gl?.deleteFramebuffer(fbo.framebuffer)
+ gl?.deleteTexture(fbo.texture)
+ }
+ downsampleFbos = []
+ gl?.deleteShader(downsampleShader)
+ downsampleShader = null
+ gl?.deleteProgram(downsampleProgram)
+ downsampleProgram = null
+ for (const fbo of drawFbos ?? []) {
+ gl?.deleteFramebuffer(fbo?.framebuffer ?? null)
+ gl?.deleteTexture(fbo?.texture ?? null)
+ }
+ drawFbos = []
+ for (const query of queries ?? []) {
+ gl?.deleteQuery(query)
+ }
+ queries = []
+ gl?.deleteBuffer(positionBuffer)
+ positionBuffer = null
+ gl?.deleteShader(drawShader)
+ drawShader = null
+ gl?.deleteShader(vertexShader)
+ vertexShader = null
+ gl?.deleteProgram(drawProgram)
+ drawProgram = null
+ logger.log('reset done')
+}
+
+function init (hash: Uint32Array, difficulty: bigint): void {
+ if (gl == null) {
+ throw new Error('WebGL 2 is required')
+ }
+
+ // Clear any previous results
+ for (const drawFbo of drawFbos) {
+ gl.bindFramebuffer(gl.FRAMEBUFFER, drawFbo.framebuffer)
+ gl.clearBufferuiv(gl.COLOR, 0, [0, 0, 0, 0])
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null)
+ }
+ pixels.fill(0)
+ resultArray.fill(0n)
+ inputArray.fill(0)
+
+ // Set up INPUT which must be 16 bytes per 32-bit value due to array alignment
+ for (let i = 0; i < 8; i++) {
+ inputHashView.setUint32(i * 16, hash[i])
+ }
+ inputDifficultyView.setBigUint64(0, difficulty, true)
+ logger.log('INPUT', inputArray.buffer.slice(0))
+ gl.bindBuffer(gl.UNIFORM_BUFFER, inputBuffer)
+ gl.bufferSubData(gl.UNIFORM_BUFFER, 0, inputView)
+ gl.bindBuffer(gl.UNIFORM_BUFFER, null)
+
+ // Set GPU to draw mode
+ gl.useProgram(drawProgram)
+}
+
+function draw (seed: bigint, drawFbo: FBO, query: WebGLQuery): void {
+ if (gl == null) throw new Error('WebGL 2 is required to draw')
+ if (drawFbo == null) throw new Error('FBO is required to draw')
+ if (query == null) throw new Error('Query is required to draw')
+ logger.log(bigintToHex(seed, 16))
+
+ // Upload work seed buffer
+ inputSeedView.setBigUint64(0, seed, true)
+ logger.log('INPUT', inputView.buffer.slice(0))
+ gl.bindBuffer(gl.UNIFORM_BUFFER, inputBuffer)
+ gl.bufferSubData(gl.UNIFORM_BUFFER, 136, inputSeedView)
+ gl.bindBuffer(gl.UNIFORM_BUFFER, null)
+
+ // Draw full canvas
+ gl.bindFramebuffer(gl.FRAMEBUFFER, drawFbo.framebuffer)
+ gl.viewport(0, 0, drawFbo.size.x, drawFbo.size.y)
+ gl.beginQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE, query)
+ gl.drawArrays(gl.TRIANGLES, 0, 4)
+ gl.endQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE)
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null)
+}
+
+async function check (query: WebGLQuery): Promise<boolean> {
+ return new Promise((resolve, reject) => {
+ function check () {
+ try {
+ if (gl == null) throw new Error('WebGL 2 is required to check query results')
+ if (query == null) throw new Error('Query is required to check query results')
+ if (isContextLost) throw new Error('WebGL 2 context must be restored to check query results')
+ if (gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE)) {
+ resolve(!!(gl.getQueryParameter(query, gl.QUERY_RESULT)))
+ } else {
+ // Query result not yet available, check again in the next frame
+ raf = requestAnimationFrame(check)
+ }
+ } catch (err) {
+ clearTimeout(raf)
+ raf = 0
+ reject(err)
+ }
+ }
+ check()
+ })
+}
+
+/**
+* When a result is found by the `gl.query`, downsamples the texture to speed
+* up the subsequent `readPixels` call, reads the pixels into the work buffer,
+* checks every 4th pixel for the 'found' byte, converts the subsequent two
+* pixels with the nonce byte values to a hex string, and returns the result.
+*
+* @returns Nonce as an 8-byte (16-char) hexadecimal string
+*/
+function read (drawFbo: FBO): { work: bigint, difficulty: bigint } {
+ if (gl == null) throw new Error('WebGL 2 is required to read pixels')
+ if (drawFbo == null) throw new Error('Source FBO is required to downsample')
+
+ // Set GPU to downsample mode
+ gl.useProgram(downsampleProgram)
+
+ // Progressively reduce framebuffer size by half for each FBO
+ let source = drawFbo
+ gl.activeTexture(gl.TEXTURE0)
+ gl.uniform1i(downsampleSrcLocation, 0)
+ for (const fbo of downsampleFbos) {
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo.framebuffer)
+ gl.bindTexture(gl.TEXTURE_2D, source.texture)
+ gl.viewport(0, 0, fbo.size.x, fbo.size.y)
+ gl.drawArrays(gl.TRIANGLES, 0, 4)
+ source = fbo
+ }
+
+ // Read downsampled result
+ gl.bindFramebuffer(gl.FRAMEBUFFER, source.framebuffer)
+ gl.readPixels(0, 0, source.size.x, source.size.y, gl.RGBA_INTEGER, gl.UNSIGNED_INT, pixels)
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null)
+
+ // Loop pixels to find one with values
+ for (let i = 0; i < pixels.length; i += 4) {
+ if (pixels[i] || pixels[i + 1] || pixels[i + 2] || pixels[i + 3]) {
+ logger.log(`rgba(${pixels[i]}, ${pixels[i + 1]}, ${pixels[i + 2]}, ${pixels[i + 3]})`)
+ resultView.setUint32(0, pixels[i], true)
+ resultView.setUint32(4, pixels[i + 1], true)
+ resultView.setUint32(8, pixels[i + 2], true)
+ resultView.setUint32(12, pixels[i + 3], true)
+ return {
+ work: resultView.getBigUint64(0, true),
+ difficulty: resultView.getBigUint64(8, true)
+ }
+ }
+ }
+ logger.log(`pixels[${pixels.length}]`, pixels)
+ throw new Error('Query reported result but nonce value not found')
+}
+
+/**
+* Nano proof-of-work using WebGL 2.0.
+*/
+export async function generate (hash: bigint, difficulty: bigint, effort: number, debug: boolean): Promise<WorkGenerateResponse> {
+ logger.isEnabled = debug
+ // Set up 60s timeout to prevent long-running calls
+ let timeout = false
+ const kill = setTimeout(() => {
+ timeout = true
+ logger.log('timed out')
+ }, 60_000)
+ logger.groupStart('NanoPow WebGL work_generate')
+ logger.log('generating')
+
+ // Start drawing to calculate one nonce per pixel
+ let found = false
+ let result: { [key: string]: bigint } = {}
+ let isFirstRetry = false
+ try {
+ do {
+ try {
+ if (isReady === false || effort !== drawEffort || isFirstRetry) {
+ setup(effort)
+ }
+ init(bigintAsUintNArray(hash, 32, 8), difficulty)
+ logger.log('drawing frame 0')
+ draw(bigintRandom(), drawFbos[0], queries[0])
+ draw(bigintRandom(), drawFbos[1], queries[1])
+ draw(bigintRandom(), drawFbos[2], queries[2])
+ let drawIndex = 3
+ do {
+ logger.log(`drawing frame ${drawIndex}`)
+ draw(bigintRandom(), drawFbos[drawIndex], queries[drawIndex])
+ drawIndex = (drawIndex + 1) % 4
+ logger.log(`checking frame ${drawIndex}`)
+ found = await check(queries[drawIndex])
+ } while (!found && !timeout)
+ if (found) result = read(drawFbos[drawIndex])
+ isFirstRetry = false
+ } catch (err) {
+ isFirstRetry = !isFirstRetry
+ logger.log(err)
+ if (!isFirstRetry) {
+ throw new Error('failed to restore context', { cause: err })
+ }
+ while (isContextLost) await new Promise(r => setTimeout(r, 100))
+ }
+ } while (isFirstRetry)
+ } finally {
+ clearTimeout(kill)
+ cancelAnimationFrame(raf)
+ raf = 0
+ logger.groupEnd('NanoPow WebGL work_generate')
+ if (!found) throw new Error(timeout ? 'timed out' : 'work not found for unknown reason')
+ }
+
+ return {
+ hash: bigintToHex(hash, 64),
+ work: bigintToHex(result.work, 16),
+ difficulty: bigintToHex(result.difficulty, 16)
+ }
+}
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+const blake2b_state: number[][] = [
+ [0, 4, 8, 12],
+ [1, 5, 9, 13],
+ [2, 6, 10, 14],
+ [3, 7, 11, 15],
+ [0, 5, 10, 15],
+ [1, 6, 11, 12],
+ [2, 7, 8, 13],
+ [3, 4, 9, 14]
+]
+
+const blake2b_sigma: (0 | 1 | 2 | 3 | 4 | 'Z')[][] = [
+ [0, 1, 2, 3, 4, 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z'],
+ ['Z', 'Z', 4, 'Z', 'Z', 'Z', 'Z', 'Z', 1, 'Z', 0, 2, 'Z', 'Z', 'Z', 3],
+ ['Z', 'Z', 'Z', 0, 'Z', 2, 'Z', 'Z', 'Z', 'Z', 3, 'Z', 'Z', 1, 'Z', 4],
+ ['Z', 'Z', 3, 1, 'Z', 'Z', 'Z', 'Z', 2, 'Z', 'Z', 'Z', 4, 0, 'Z', 'Z'],
+ ['Z', 0, 'Z', 'Z', 2, 4, 'Z', 'Z', 'Z', 1, 'Z', 'Z', 'Z', 'Z', 3, 'Z'],
+ [2, 'Z', 'Z', 'Z', 0, 'Z', 'Z', 3, 4, 'Z', 'Z', 'Z', 'Z', 'Z', 1, 'Z'],
+ ['Z', 'Z', 1, 'Z', 'Z', 'Z', 4, 'Z', 0, 'Z', 'Z', 3, 'Z', 2, 'Z', 'Z'],
+ ['Z', 'Z', 'Z', 'Z', 'Z', 1, 3, 'Z', 'Z', 0, 'Z', 4, 'Z', 'Z', 2, 'Z'],
+ ['Z', 'Z', 'Z', 'Z', 'Z', 3, 0, 'Z', 'Z', 2, 'Z', 'Z', 1, 4, 'Z', 'Z'],
+ ['Z', 2, 'Z', 4, 'Z', 'Z', 1, 'Z', 'Z', 'Z', 'Z', 'Z', 3, 'Z', 'Z', 0],
+ [0, 1, 2, 3, 4, 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z'],
+ ['Z', 'Z', 4, 'Z', 'Z', 'Z', 'Z', 'Z', 1, 'Z', 0, 2, 'Z', 'Z', 'Z', 3]
+]
+
+/**
+* Initialization vector defined by BLAKE2.
+* Application of each XOR is defined by BLAKE2 section 2.4 compression function.
+* Each uvec2 represents two halves of the original u64 value from the reference
+* implementation. They appear reversed pairwise as defined below, but this is an
+* illusion due to endianness: the \`x\` component of the vector is the low bits
+* and the \`y\` component is the high bits, and if you laid the bits out
+* individually, they would match the little-endian 64-bit representation.
+*/
+const blake2b_iv = [
+ 'uvec2(0xF3BCC908u, 0x6A09E667u)',
+ 'uvec2(0x84CAA73Bu, 0xBB67AE85u)',
+ 'uvec2(0xFE94F82Bu, 0x3C6EF372u)',
+ 'uvec2(0x5F1D36F1u, 0xA54FF53Au)',
+ 'uvec2(0xADE682D1u, 0x510E527Fu)',
+ 'uvec2(0x2B3E6C1Fu, 0x9B05688Cu)',
+ 'uvec2(0xFB41BD6Bu, 0x1F83D9ABu)',
+ 'uvec2(0x137E2179u, 0x5BE0CD19u)'
+]
+
+/**
+* Parameter block as defined in BLAKE2 section 2.8 and configured as follows:
+* maximal depth = 1, fanout = 1, digest byte length = 8
+*/
+const blake2b_param = `uvec2(0x01010008u, 0u)`
+
+function G (a: number, b: number, c: number, d: number,
+ x: 0 | 1 | 2 | 3 | 4 | 'Z', y: 0 | 1 | 2 | 3 | 4 | 'Z'
+): string {
+ return `
+v${a} += v${b};
+v${a}.y += uint(v${a}.x < v${b}.x);
+
+${x === 'Z' ? `// NOP` : `
+ v${a} += m${x};
+ v${a}.y += uint(v${a}.x < m${x}.x);`}
+
+v${d} = (v${d} ^ v${a}).yx;
+
+v${c} += v${d};
+v${c}.y += uint(v${c}.x < v${d}.x);
+
+v${b} ^= v${c};
+v${b} = (v${b} >> uvec2(24u)) | (v${b} << uvec2(8u)).yx;
+
+v${a} += v${b};
+v${a}.y += uint(v${a}.x < v${b}.x);
+
+${y === 'Z' ? `// NOP` : `
+ v${a} += m${y};
+ v${a}.y += uint(v${a}.x < m${y}.x);`}
+
+v${d} ^= v${a};
+v${d} = (v${d} >> uvec2(16u)) | (v${d} << uvec2(16u)).yx;
+
+v${c} += v${d};
+v${c}.y += uint(v${c}.x < v${d}.x);
+
+v${b} ^= v${c};
+v${b} = (v${b} >> uvec2(31u)).yx | (v${b} << uvec2(1u));
+`}
+
+function ROUND (r: number): string {
+ let output = `// ROUND ${r}`
+ for (let i = 0; i < 8; i++) {
+ const [a, b, c, d] = blake2b_state[i]
+ const s = blake2b_sigma[r]
+ output += r === 11 && (i === 5 || i === 7) ? `// G NOP` : `
+ ${G(a, b, c, d, s[2 * i], s[2 * i + 1])}
+ `
+ }
+ return output
+}
+
+function SETUP (): string {
+ let output = `
+// Initialize fragment output
+work = uvec4(0u);
+
+// Initialize unique nonce
+uvec2 m0 = seed ^ uvec2(gl_FragCoord);
+
+// Block hash
+uvec2 m1 = uvec2(hash[0u], hash[1u]);
+uvec2 m2 = uvec2(hash[2u], hash[3u]);
+uvec2 m3 = uvec2(hash[4u], hash[5u]);
+uvec2 m4 = uvec2(hash[6u], hash[7u]);
+
+// Initialize state vector configured for Nano
+// v0: depth=1; fanout=1; outlen=8
+// v12: input byte length
+// v14: final block flag
+`
+ for (let i = 0; i < 8; i++) {
+ output += `
+ uvec2 v${i} = ${blake2b_iv[i]};
+ uvec2 v${i + 8} = ${blake2b_iv[i]};
+ `
+ }
+ output += `
+ v0 ^= uvec2(0x01010008u, 0u);
+ v12 ^= uvec2(40u, 0u);
+ v14 = ~v14;
+ `
+ return output
+}
+
+function HASH () {
+ let output = `
+/**
+* Twelve rounds of G mixing as part of BLAKE2b compression step, each divided
+* into eight subprocesses, each of which is further paired to be processed in
+* parallel by packing independent uvec2 variables into vec4 variables.
+* Each subprocess statement execution is alternated so that the compiler can
+* interleave independent instructions for improved scheduling. That is to say,
+* the first statement \`a = a + b\` is executed for each subprocess, and then
+* the next statement \`a = a + m[sigma[r][2*i+0]]\` is executed, and so on
+* through all the steps of the G mix function. Once subprocesses 1-4 are done,
+* computation on subprocesses 5-8 are executed in the same manner.
+*
+* Each subprocess applies transformations to \`m\` and \`v\` variables based on a
+* defined set of index inputs. The algorithm for each subprocess is defined as
+* follows:
+*
+* r is the current round
+* i is the current subprocess within that round
+* a, b, c, d are elements of \`v\` at specific indexes
+* sigma is a defined set of array indexes for \`m\`
+* rotr64 is a right-hand bit rotation function
+*
+* a = a + b
+* a = a + m[sigma[r][2*i+0]]
+* d = rotr64(d ^ a, 32)
+* c = c + d
+* b = rotr64(b ^ c, 24)
+* a = a + b
+* a = a + m[sigma[r][2*i+1]]
+* d = rotr64(d ^ a, 16)
+* c = c + d
+* b = rotr64(b ^ c, 63)
+*
+* Each sum step has an extra carry addition. Note that the m[sigma] sum is
+* skipped if m[sigma] is zero since it effectively does nothing. Also note
+* that rotations must be applied differently from the reference implementation
+* due to the lack of both a native rotate function and 64-bit support in WGSL.
+*/
+`
+ for (let i = 0; i < 12; i++) output += ROUND(i)
+ return output
+}
+
+function UNIFORMS () {
+ return `
+// hash - Array of 32-bit integers comprising a 32-byte Nano block hash
+// difficulty - Minimum threshold for BLAKE2b result for work to be valid
+// seed - Random value which is uniquely varied by pixel coordinates
+layout(std140) uniform INPUT {
+ uint hash[8];
+ uvec2 difficulty;
+ uvec2 seed;
+};
+
+// work - Pixel value output if and only if valid nonce is found
+out uvec4 work;
+`}
+
+/** Output to draw.frag using Node */
+console.log(`#version 300 es
+#pragma vscode_glsllint_stage: frag
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-FileContributor: Ben Green <ben@latenightsketches.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later AND MIT
+
+#ifdef GL_FRAGMENT_PRECISION_HIGH
+precision highp float;
+#else
+precision mediump float;
+#endif
+${UNIFORMS()}
+
+/**
+* Main draw function
+*
+* Draws a single pixel per shader invocation, multiplied by the dimensions of
+* the canvas.
+*
+* Each component of a random 8-byte value, provided by the INPUT as a uvec2,
+* is XOR'd with the 2-D coordinates of the pixel on the canvas to create a
+* unique nonce value for each.
+*
+* Where the reference implementation uses array lookups, the NanoPow
+* implementation assigns each array element to its own variable to enhance
+* performance, but the variable name still contains the original index digit.
+*/
+void main() {
+
+ ${SETUP()}
+ ${HASH()}
+
+ // NONCE CHECK
+ // Set pixel value if it exceeds difficulty threshold, else discard it.
+ uvec2 result = ${blake2b_iv[0]} ^ ${blake2b_param} ^ v0 ^ v8;
+ if (result.y > difficulty.y || (result.y == difficulty.y && result.x >= difficulty.x)) {
+ work = uvec4(m0, result);
+ }
+ if (work.x == 0u) {
+ discard;
+ }
+}
+`)
+
+export { }
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { default as downsampleSource } from './downsample.frag'
+import { default as drawSource } from './build/draw.frag'
+import { default as quadSource } from './quad.vert'
+
+export { downsampleSource, drawSource, quadSource }
--- /dev/null
+{
+ "include": [
+ "./*.ts"
+ ],
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "outDir": "./build"
+ }
+}
--- /dev/null
+SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+SPDX-License-Identifier: GPL-3.0-or-later
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+declare module '*.frag' {
+ const value: string
+ export default value
+}
+
+declare module '*.glsl' {
+ const value: string
+ export default value
+}
+
+declare module '*.vert' {
+ const value: string
+ export default value
+}
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { default as NanoPowGpuComputeShader } from './shaders/build/compute.wgsl'
+import { WorkGenerateResponse } from '#types'
+import { bigintAsUintNArray, bigintRandom, bigintToHex, Logger, Queue } from '#utils'
+
+type NanoPowDeviceStatus = 'Idle' | 'Starting' | 'Unsupported' | 'Ready' | 'Restoring' | 'Crashed'
+
+const logger = new Logger()
+const q = new Queue()
+
+// Initialize host buffers
+const hashData: BigUint64Array = new BigUint64Array(4)
+const bufferReset: BigUint64Array = new BigUint64Array(4)
+const inputData: BigUint64Array = new BigUint64Array(6)
+const inputDataView: DataView = new DataView(inputData.buffer)
+let resultData: Uint32Array = new Uint32Array(5)
+let resultView: DataView = new DataView(resultData.buffer)
+
+// Initialize process variables
+let isContextLost: number = 0
+let status: NanoPowDeviceStatus = 'Idle'
+
+// Declare WebGPU variables
+let device: GPUDevice
+let bindGroupLayout: GPUBindGroupLayout | null
+let bindGroup: GPUBindGroup | null
+let pipeline: GPUComputePipeline
+let inputBuffer: GPUBuffer
+let outputBuffer: GPUBuffer
+let resultBuffer: GPUBuffer
+
+// Initialize WebGPU
+async function start (): Promise<void> {
+ if (status === 'Idle') {
+ logger.log('starting')
+ status = 'Starting'
+ await getDevice()
+ try {
+ await compile()
+ logger.log('ready')
+ status = 'Ready'
+ } catch (err) {
+ status = 'Crashed'
+ logger.log(err)
+ throw new Error('failed to compile', { cause: err })
+ }
+ }
+}
+
+// Request device and adapter
+async function getDevice () {
+ if (navigator.gpu == null) {
+ status = 'Unsupported'
+ throw new Error('WebGPU is not supported in this browser.')
+ }
+ const adapter = await navigator.gpu.requestAdapter()
+ if (adapter == null) {
+ status = 'Unsupported'
+ throw new Error('gpu adapter refused by browser')
+ }
+ device = await adapter.requestDevice()
+ if (!(device instanceof GPUDevice)) {
+ throw new Error('failed to get device from gpu adapter')
+ }
+ device.lost?.then(async (deviceLostInfo) => {
+ logger.log('device lost', deviceLostInfo)
+ // Set up 30s timeout to prevent long-running calls
+ isContextLost = window.setTimeout(() => {
+ throw new Error('failed to restore device', { cause: deviceLostInfo })
+ }, 30_000)
+ if (status !== 'Restoring') await q.prioritize(restore)
+ })
+}
+
+// Compile and cache shader prior to actual dispatch
+async function compile () {
+ // Create buffers for writing GPU calculations and reading from Javascript
+ inputBuffer = device.createBuffer({
+ label: 'INPUT',
+ size: 48,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
+ })
+ outputBuffer = device.createBuffer({
+ label: 'gpu',
+ size: 32,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
+ })
+ resultBuffer = device.createBuffer({
+ label: 'cpu',
+ size: 32,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
+ })
+ // Create binding group data structure to use later once INPUT is known
+ bindGroupLayout = device.createBindGroupLayout({
+ entries: [
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' }, },
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' }, },
+ ],
+ })
+ // Bind INPUT read and GPU write buffers
+ bindGroup = device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: [
+ { binding: 0, resource: { buffer: inputBuffer }, },
+ { binding: 1, resource: { buffer: outputBuffer }, },
+ ],
+ })
+ // Create pipeline to connect compute shader to binding layout
+ pipeline = device.createComputePipeline({
+ layout: device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout]
+ }),
+ compute: {
+ entryPoint: 'main',
+ module: device.createShaderModule({
+ code: NanoPowGpuComputeShader
+ })
+ }
+ })
+ const cmd = device.createCommandEncoder()
+ cmd.beginComputePass().end()
+ device.queue.submit([cmd.finish()])
+ await device.queue.onSubmittedWorkDone()
+}
+
+async function restore (): Promise<void> {
+ logger.log('restoring')
+ try {
+ status = 'Restoring'
+ try { resultBuffer?.unmap() } catch { }
+ resultBuffer?.destroy()
+ outputBuffer?.destroy()
+ inputBuffer?.destroy()
+ bindGroupLayout = null
+ bindGroup = null
+ await getDevice()
+ await compile()
+ window.clearTimeout(isContextLost)
+ isContextLost = 0
+ logger.log('ready')
+ status = 'Ready'
+ } catch (err) {
+ status = 'Crashed'
+ throw new Error('failed to restore device', { cause: err })
+ }
+}
+
+async function init (hash: BigUint64Array, difficulty: bigint): Promise<void> {
+ logger.log('variables initializing')
+ try {
+ // Save hash data for normal usage and potential recovery efforts
+ hashData.set(hash)
+
+ // Write data that will not change per dispatch to uniform buffer object
+ // Note: u64 size is 8, but total alignment must be multiple of 16
+ inputData.fill(0n)
+ for (let i = 0; i < 4; i++) {
+ inputDataView.setBigUint64(i * 8, hashData[i])
+ }
+ inputDataView.setBigUint64(32, difficulty, true)
+ device.queue.writeBuffer(inputBuffer, 0, inputDataView)
+
+ // Reset OUTPUT properties to 0u before each calculation
+ device.queue.writeBuffer(outputBuffer, 0, bufferReset)
+ device.queue.writeBuffer(resultBuffer, 0, bufferReset)
+ } catch (err) {
+ logger.log(err)
+ throw new Error('failed to initialize', { cause: err })
+ }
+}
+
+async function dispatch (seed: bigint, effort: number): Promise<void> {
+ logger.log('dispatching compute pass')
+ try {
+ logger.log('seed', bigintToHex(seed, 16))
+
+ // Copy seed into INPUT buffer
+ inputDataView.setBigUint64(40, seed, true)
+ logger.log('INPUT', inputDataView)
+ device.queue.writeBuffer(inputBuffer, 0, inputDataView)
+
+ // Create command encoder to issue commands to GPU and initiate computation
+ const commandEncoder = device.createCommandEncoder()
+ const passEncoder = commandEncoder.beginComputePass()
+
+ // Issue commands and end compute pass structure
+ passEncoder.setPipeline(pipeline)
+ passEncoder.setBindGroup(0, bindGroup)
+ passEncoder.dispatchWorkgroups(effort * 0x100, effort * 0x100)
+ passEncoder.end()
+
+ // Copy 8-byte result, 8-byte nonce, and 4-byte found flag from GPU to CPU
+ // for reading
+ commandEncoder.copyBufferToBuffer(outputBuffer, 0, resultBuffer, 0, 32)
+
+ // End computation by passing array of command buffers to command queue for execution
+ device.queue.submit([commandEncoder.finish()])
+ } catch (err) {
+ logger.log(err)
+ throw new Error('failed to dispatch compute pass', { cause: err })
+ }
+}
+
+async function check (): Promise<boolean> {
+ logger.log('checking results from compute pass')
+ try {
+ await resultBuffer.mapAsync(GPUMapMode.READ)
+ await device.queue.onSubmittedWorkDone()
+ resultData = new Uint32Array(resultBuffer.getMappedRange().slice(0))
+ resultBuffer.unmap()
+ resultView = new DataView(resultData.buffer)
+ logger.log('OUTPUT', resultView)
+ if (resultView == null) throw new Error('failed to get data from resultBuffer.')
+ return !!resultView.getUint32(0, true)
+ } catch (err) {
+ logger.log(err)
+ throw new Error('failed to read results from compute pass', { cause: err })
+ }
+}
+
+/**
+* Map CPU buffer to GPU, read results to static result object, and unmap.
+*/
+function read (): { work: bigint, difficulty: bigint } {
+ logger.log('reading results from compute pass')
+ try {
+ if (resultView == null) throw new Error('failed to get data from result view')
+ return {
+ work: resultView.getBigUint64(8, true),
+ difficulty: resultView.getBigUint64(16, true)
+ }
+ } catch (err) {
+ logger.log(err)
+ throw new Error('failed to read results from compute pass', { cause: err })
+ }
+}
+
+/**
+* Nano proof-of-work using WebGPU.
+*/
+export async function generate (hash: bigint, difficulty: bigint, effort: number, debug: boolean): Promise<WorkGenerateResponse> {
+ logger.isEnabled = debug
+ // Set up 60s timeout to prevent long-running calls
+ let timeout = false
+ const kill = setTimeout(() => {
+ timeout = true
+ throw new Error('timed out')
+ }, 60_000)
+ logger.groupStart('NanoPow WebGPU work_generate')
+ logger.log('generating')
+ let found = false
+ let result: { [key: string]: bigint } = {}
+ let isFirstRetry = false
+ try {
+ do {
+ try {
+ // Start device and initialize variables
+ if (status === 'Idle' || isFirstRetry) {
+ await q.add(start)
+ if (status !== 'Ready') {
+ throw new Error('failed to start')
+ }
+ }
+ await q.add(init, bigintAsUintNArray(hash, 64, 4), difficulty)
+ // Loop attempts until valid work found
+ do {
+ await dispatch(bigintRandom(), effort)
+ found = await check()
+ } while (!found && !timeout)
+ if (found) result = read()
+ isFirstRetry = false
+ } catch (err: any) {
+ if (status === 'Unsupported') {
+ throw new Error(err.message, { cause: err })
+ }
+ // Only retry once here, allow errors to propagate out to the consumer
+ isFirstRetry = !isFirstRetry
+ logger.log(err)
+ if (!isFirstRetry) {
+ throw new Error('failed to restore device', { cause: err })
+ }
+ while (isContextLost) await new Promise(r => setTimeout(r, 100))
+ }
+ } while (isFirstRetry)
+ } finally {
+ clearTimeout(kill)
+ logger.groupEnd('NanoPow WebGPU work_generate')
+ if (!found && timeout) throw new Error('timed out')
+ }
+
+ return {
+ hash: bigintToHex(hash, 64),
+ work: bigintToHex(result.work, 16),
+ difficulty: bigintToHex(result.difficulty, 16)
+ }
+}
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+const blake2b_state: number[][] = [
+ [0, 4, 8, 12],
+ [1, 5, 9, 13],
+ [2, 6, 10, 14],
+ [3, 7, 11, 15],
+ [0, 5, 10, 15],
+ [1, 6, 11, 12],
+ [2, 7, 8, 13],
+ [3, 4, 9, 14]
+]
+
+const blake2b_sigma: (0 | 1 | 2 | 3 | 4 | 'Z')[][] = [
+ [0, 1, 2, 3, 4, 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z'],
+ ['Z', 'Z', 4, 'Z', 'Z', 'Z', 'Z', 'Z', 1, 'Z', 0, 2, 'Z', 'Z', 'Z', 3],
+ ['Z', 'Z', 'Z', 0, 'Z', 2, 'Z', 'Z', 'Z', 'Z', 3, 'Z', 'Z', 1, 'Z', 4],
+ ['Z', 'Z', 3, 1, 'Z', 'Z', 'Z', 'Z', 2, 'Z', 'Z', 'Z', 4, 0, 'Z', 'Z'],
+ ['Z', 0, 'Z', 'Z', 2, 4, 'Z', 'Z', 'Z', 1, 'Z', 'Z', 'Z', 'Z', 3, 'Z'],
+ [2, 'Z', 'Z', 'Z', 0, 'Z', 'Z', 3, 4, 'Z', 'Z', 'Z', 'Z', 'Z', 1, 'Z'],
+ ['Z', 'Z', 1, 'Z', 'Z', 'Z', 4, 'Z', 0, 'Z', 'Z', 3, 'Z', 2, 'Z', 'Z'],
+ ['Z', 'Z', 'Z', 'Z', 'Z', 1, 3, 'Z', 'Z', 0, 'Z', 4, 'Z', 'Z', 2, 'Z'],
+ ['Z', 'Z', 'Z', 'Z', 'Z', 3, 0, 'Z', 'Z', 2, 'Z', 'Z', 1, 4, 'Z', 'Z'],
+ ['Z', 2, 'Z', 4, 'Z', 'Z', 1, 'Z', 'Z', 'Z', 'Z', 'Z', 3, 'Z', 'Z', 0],
+ [0, 1, 2, 3, 4, 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z'],
+ ['Z', 'Z', 4, 'Z', 'Z', 'Z', 'Z', 'Z', 1, 'Z', 0, 2, 'Z', 'Z', 'Z', 3]
+]
+
+/**
+* Initialization vector defined by BLAKE2.
+* Application of each XOR is defined by BLAKE2 section 2.4 compression function.
+* Each vec2<u32> represents two halves
+* of the original u64 value from the reference implementation. They appear
+* reversed pairwise as defined below, but this is an illusion due to endianness:
+* the \`x\` component of the vector is the low bits and the \`y\` component is the
+* high bits, and if you laid the bits out individually, they would match the
+* little-endian 64-bit representation.
+*/
+const blake2b_iv = [
+ '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)'
+]
+
+/**
+* Parameter block as defined in BLAKE2 section 2.8 and configured as follows:
+* maximal depth = 1, fanout = 1, digest byte length = 8
+*/
+const blake2b_param = `vec2<u32>(0x01010008u, 0u)`
+
+function G (a: number, b: number, c: number, d: number,
+ x: 0 | 1 | 2 | 3 | 4 | 'Z', y: 0 | 1 | 2 | 3 | 4 | 'Z'
+): string {
+ return `
+v${a} += v${b};
+v${a}.y += u32(v${a}.x < v${b}.x);
+
+${x === 'Z' ? `// NOP` : `
+ v${a} += m${x};
+ v${a}.y += u32(v${a}.x < m${x}.x);`}
+
+v${d} = (v${d} ^ v${a}).yx;
+
+v${c} += v${d};
+v${c}.y += u32(v${c}.x < v${d}.x);
+
+v${b} ^= v${c};
+v${b} = (v${b} >> vec2(24u)) | (v${b} << vec2(8u)).yx;
+
+v${a} += v${b};
+v${a}.y += u32(v${a}.x < v${b}.x);
+
+${y === 'Z' ? `// NOP` : `
+ v${a} += m${y};
+ v${a}.y += u32(v${a}.x < m${y}.x);`}
+
+v${d} ^= v${a};
+v${d} = (v${d} >> vec2(16u)) | (v${d} << vec2(16u)).yx;
+
+v${c} += v${d};
+v${c}.y += u32(v${c}.x < v${d}.x);
+
+v${b} ^= v${c};
+v${b} = (v${b} >> vec2(31u)).yx | (v${b} << vec2(1u));
+`}
+
+function ROUND (r: number): string {
+ let output = `// ROUND ${r}`
+ for (let i = 0; i < 8; i++) {
+ const [a, b, c, d] = blake2b_state[i]
+ const s = blake2b_sigma[r]
+ output += r === 11 && (i === 5 || i === 7) ? `// G NOP` : `
+ ${G(a, b, c, d, s[2 * i], s[2 * i + 1])}
+ `
+ }
+ return output
+}
+
+function SETUP (): string {
+ let output = `
+// Initialize unique nonce
+let m0: vec2<u32> = seed ^ global_id.xy;
+
+// Initialize state vector configured for Nano
+// v0: depth=1; fanout=1; outlen=8
+// v12: input byte length
+// v14: final block flag
+`
+ for (let i = 0; i < 8; i++) {
+ output += `
+ var v${i}: vec2<u32> = ${blake2b_iv[i]};
+ var v${i + 8}: vec2<u32> = ${blake2b_iv[i]};
+ `
+ }
+ output += `
+ v0 ^= vec2(0x01010008u, 0u);
+ v12 ^= vec2(40u, 0u);
+ v14 = ~v14;
+ `
+ return output
+}
+
+function HASH () {
+ let output = `
+/**
+* Twelve rounds of G mixing as part of BLAKE2b compression step, each divided
+* into eight subprocesses, each of which is further paired to be processed in
+* parallel by packing independent vec2 variables into vec4 variables.
+* Each subprocess statement execution is alternated so that the compiler can
+* interleave independent instructions for improved scheduling. That is to say,
+* the first statement \`a = a + b\` is executed for each subprocess, and then
+* the next statement \`a = a + m[sigma[r][2*i+0]]\` is executed, and so on
+* through all the steps of the G mix function. Once subprocesses 1-4 are done,
+* computation on subprocesses 5-8 are executed in the same manner.
+*
+* Each subprocess applies transformations to \`m\` and \`v\` variables based on a
+* defined set of index inputs. The algorithm for each subprocess is defined as
+* follows:
+*
+* r is the current round
+* i is the current subprocess within that round
+* a, b, c, d are elements of \`v\` at specific indexes
+* sigma is a defined set of array indexes for \`m\`
+* rotr64 is a right-hand bit rotation function
+*
+* a = a + b
+* a = a + m[sigma[r][2*i+0]]
+* d = rotr64(d ^ a, 32)
+* c = c + d
+* b = rotr64(b ^ c, 24)
+* a = a + b
+* a = a + m[sigma[r][2*i+1]]
+* d = rotr64(d ^ a, 16)
+* c = c + d
+* b = rotr64(b ^ c, 63)
+*
+* Each sum step has an extra carry addition. Note that the m[sigma] sum is
+* skipped if m[sigma] is zero since it effectively does nothing. Also note
+* that rotations must be applied differently from the reference implementation
+* due to the lack of both a native rotate function and 64-bit support in WGSL.
+*/
+`
+ for (let i = 0; i < 12; i++) output += ROUND(i)
+ return output
+}
+
+function UNIFORMS () {
+ return `
+// Input buffers
+struct INPUT {
+ hash: array<vec4<u32>, 2>,
+ difficulty: vec2<u32>,
+ seed: vec2<u32>
+};
+@group(0) @binding(0) var<uniform>input: INPUT;
+
+// Output buffers
+struct OUTPUT {
+ found: atomic<u32>,
+ work: vec2<u32>,
+ difficulty: vec2<u32>
+};
+@group(0) @binding(1) var<storage, read_write > output: OUTPUT;
+`}
+
+function SHARED () {
+ return `
+// Used to fill partial \`m\` vec2 constructions.
+const mZ = vec2(0u);
+
+// Shared flag to prevent execution for all workgroup threads based on the
+// atomicLoad() result of a single member thread.
+var<workgroup> found: bool;
+
+// Shared memory for difficulty, hash, and seed.
+var<workgroup> m1: vec2<u32>;
+var<workgroup> m2: vec2<u32>;
+var<workgroup> m3: vec2<u32>;
+var<workgroup> m4: vec2<u32>;
+var<workgroup> d: vec2<u32>;
+var<workgroup> seed: vec2<u32>;
+`}
+
+/** Output to compute.wgsl using Node */
+console.log(`//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+${UNIFORMS()}
+${SHARED()}
+
+/**
+* Main compute function
+*
+* Computes with a workgroup size of 64 which balances warps between NVIDIA and
+* AMD cards while still considering the power-sensitive requirements of mobile
+* devices. The entire workgroup exits immediately if a nonce was already found
+* by a previous workgroup.
+*
+* Each component of a random 8-byte value, provided by the UBO as a vec2<u32>,
+* is XOR'd with a different dimensional index from the global thread identifier
+* to create a unique nonce value for each thread.
+*
+* Where the reference implementation uses array lookups, the NanoPow
+* implementation assigns each array element to its own variable to enhance
+* performance, but the variable name still contains the original index digit.
+*/
+@compute @workgroup_size(64)
+fn main(@builtin(global_invocation_id) global_id: vec3<u32>, @builtin(local_invocation_id) local_id: vec3<u32>) {
+ if (local_id.x == 0u) {
+ found = atomicLoad(& output.found) != 0u;
+ seed = input.seed;
+ m1 = input.hash[0u].xy;
+ m2 = input.hash[0u].zw;
+ m3 = input.hash[1u].xy;
+ m4 = input.hash[1u].zw;
+ d = input.difficulty;
+ }
+ workgroupBarrier();
+ if (found) { return; }
+
+ ${SETUP()}
+ ${HASH()}
+
+ // NONCE CHECK
+ // Set nonce if it exceeds difficulty threshold and no other thread has set it.
+ let result = ${blake2b_iv[0]} ^ ${blake2b_param} ^ v0 ^ v8;
+ if (result.y > input.difficulty.y || (result.y == input.difficulty.y && result.x >= input.difficulty.x)) {
+ loop {
+ let swap = atomicCompareExchangeWeak(&output.found, 0u, 1u);
+ if (swap.exchanged) {
+ output.work = m0;
+ output.difficulty = result;
+ break;
+ }
+ if (swap.old_value != 0u) {
+ break;
+ }
+ }
+ return;
+ }
+}
+`)
+
+export { }
--- /dev/null
+{
+ "include": [
+ "./*.ts"
+ ],
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "outDir": "./build"
+ }
+}
--- /dev/null
+SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+SPDX-License-Identifier: GPL-3.0-or-later
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+declare module '*.wgsl' {
+ const value: string
+ export default value
+}
+++ /dev/null
-#version 300 es
-#pragma vscode_glsllint_stage: frag
-//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
-//! SPDX-FileContributor: Ben Green <ben@latenightsketches.com>
-//! SPDX-License-Identifier: GPL-3.0-or-later AND MIT
-
-#ifdef GL_FRAGMENT_PRECISION_HIGH
-precision highp float;
-#else
-precision mediump float;
-#endif
-
-out uvec4 nonce;
-
-// blockhash - Array of precalculated block hash components
-// difficulty - 0xfffffff800000000 for send/change blocks, 0xfffffe0000000000 for all else
-// validate - If true, checks only 1 pixel to validate, else checks all pixels to search
-layout(std140) uniform UBO {
- uint blockhash[8];
- uvec2 difficulty;
- bool validate;
-};
-
-// Random work seed values
-layout(std140) uniform WORK {
- uvec2 seed;
-};
-
-/**
-* Initialization vector defined by BLAKE2. Each vec2<u32> represents two halves
-* of the original u64 value from the reference implementation. They appear
-* reversed pairwise as defined below, but this is an illusion due to endianness:
-* the \`x\` component of the vector is the low bits and the \`y\` component is the
-* high bits, and if you laid the bits out individually, they would match the
-* little-endian 64-bit representation.
-*/
-const uvec2 BLAKE2B_IV[8] = uvec2[8](
- uvec2(0xF3BCC908u, 0x6A09E667u),
- uvec2(0x84CAA73Bu, 0xBB67AE85u),
- uvec2(0xFE94F82Bu, 0x3C6EF372u),
- uvec2(0x5F1D36F1u, 0xA54FF53Au),
- uvec2(0xADE682D1u, 0x510E527Fu),
- uvec2(0x2B3E6C1Fu, 0x9B05688Cu),
- uvec2(0xFB41BD6Bu, 0x1F83D9ABu),
- uvec2(0x137E2179u, 0x5BE0CD19u)
-);
-
-/**
-* Parameter block as defined in BLAKE2 section 2.8 and configured as follows:
-* maximal depth = 1, fanout = 1, digest byte length = 8
-*/
-const uvec2 BLAKE2B_PARAM = uvec2(0x01010008u, 0u);
-
-/**
-* Message input length which is always 40 for Nano.
-* 8 nonce bytes + 32 block hash bytes
-*/
-const uvec2 BLAKE2B_INLEN = uvec2(0x00000028u, 0u);
-
-/**
-* Finalization flag as defined in BLAKE2 section 2.4 and set to ~0 since this is
-* the final (and only) message block being hashed.
-*/
-const uvec2 BLAKE2B_FINAL = uvec2(0xFFFFFFFFu, 0xFFFFFFFFu);
-
-/**
-* Fully initialized state array that is locally copied at each thread start.
-* Application of each XOR is defined by BLAKE2 section 2.4 compression function.
-*/
-const uvec2 BLAKE2B_INIT[16] = uvec2[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]
-);
-
-// Defined separately from uint v[0].y below as the original value is required
-// to calculate the second uint32 of the digest for threshold comparison
-const uint BLAKE2B_IV32_1 = 0x6A09E667u;
-
-// Used during G for vector bit rotations
-const uvec4 ROTATE_1 = uvec4(1u);
-const uvec4 ROTATE_8 = uvec4(8u);
-const uvec4 ROTATE_16 = uvec4(16u);
-const uvec4 ROTATE_24 = uvec4(24u);
-const uvec4 ROTATE_31 = uvec4(31u);
-
-// Iterated initialization vector
-uvec2 v[16];
-
-// Input data buffer
-uvec2 m[16];
-
-// G mixing function, compressing two subprocesses into one
-void G (
- uint a0, uint b0, uint c0, uint d0, uvec2 x0, uvec2 y0,
- uint a1, uint b1, uint c1, uint d1, uvec2 x1, uvec2 y1
-) {
- uvec4 a = uvec4(v[a0], v[a1]);
- uvec4 b = uvec4(v[b0], v[b1]);
- uvec4 c = uvec4(v[c0], v[c1]);
- uvec4 d = uvec4(v[d0], v[d1]);
- uvec4 mx = uvec4(x0, x1);
- uvec4 my = uvec4(y0, y1);
-
- a = a + b + uvec4(0u, uint(a.x + b.x < a.x), 0u, uint(a.z + b.z < a.z));
- a = a + mx + uvec4(0u, uint(a.x + mx.x < a.x), 0u, uint(a.z + mx.z < a.z));
- d = (d ^ a).yxwz;
- c = c + d + uvec4(0u, uint(c.x + d.x < c.x), 0u, uint(c.z + d.z < c.z));
- b = ((b ^ c) >> ROTATE_24) | ((b ^ c) << ROTATE_8).yxwz;
- a = a + b + uvec4(0u, uint(a.x + b.x < b.x), 0u, uint(a.z + b.z < b.z));
- a = a + my + uvec4(0u, uint(a.x + my.x < a.x), 0u, uint(a.z + my.z < a.z));
- d = ((d ^ a) >> ROTATE_16) | ((d ^ a) << ROTATE_16).yxwz;
- c = c + d + uvec4(0u, uint(c.x + d.x < c.x), 0u, uint(c.z + d.z < c.z));
- b = ((b ^ c) >> ROTATE_31).yxwz | ((b ^ c) << ROTATE_1);
-
- v[a0] = a.xy;
- v[b0] = b.xy;
- v[c0] = c.xy;
- v[d0] = d.xy;
- v[a1] = a.zw;
- v[b1] = b.zw;
- v[c1] = c.zw;
- v[d1] = d.zw;
-}
-
-void main() {
- // Initialize fragment output
- nonce = uvec4(0u);
-
- // Nonce uniquely differentiated by pixel location
- m[0u] = seed ^ uvec2(gl_FragCoord);
-
- // Block hash
- m[1u] = uvec2(blockhash[0u], blockhash[1u]);
- m[2u] = uvec2(blockhash[2u], blockhash[3u]);
- m[3u] = uvec2(blockhash[4u], blockhash[5u]);
- m[4u] = uvec2(blockhash[6u], blockhash[7u]);
-
- // Reset v
- v = BLAKE2B_INIT;
-
- // Twelve rounds of G mixing
-
- // Round 0
- G(0u, 4u, 8u, 12u, m[0u], m[1u], 1u, 5u, 9u, 13u, m[2u], m[3u]);
- G(2u, 6u, 10u, 14u, m[4u], m[5u], 3u, 7u, 11u, 15u, m[6u], m[7u]);
- G(0u, 5u, 10u, 15u, m[8u], m[9u], 1u, 6u, 11u, 12u, m[10u], m[11u]);
- G(2u, 7u, 8u, 13u, m[12u], m[13u], 3u, 4u, 9u, 14u, m[14u], m[15u]);
-
- // Round 1
- G(0u, 4u, 8u, 12u, m[14u], m[10u], 1u, 5u, 9u, 13u, m[4u], m[8u]);
- G(2u, 6u, 10u, 14u, m[9u], m[15u], 3u, 7u, 11u, 15u, m[13u], m[6u]);
- G(0u, 5u, 10u, 15u, m[1u], m[12u], 1u, 6u, 11u, 12u, m[0u], m[2u]);
- G(2u, 7u, 8u, 13u, m[11u], m[7u], 3u, 4u, 9u, 14u, m[5u], m[3u]);
-
- // Round 2
- G(0u, 4u, 8u, 12u, m[11u], m[8u], 1u, 5u, 9u, 13u, m[12u], m[0u]);
- G(2u, 6u, 10u, 14u, m[5u], m[2u], 3u, 7u, 11u, 15u, m[15u], m[13u]);
- G(0u, 5u, 10u, 15u, m[10u], m[14u], 1u, 6u, 11u, 12u, m[3u], m[6u]);
- G(2u, 7u, 8u, 13u, m[7u], m[1u], 3u, 4u, 9u, 14u, m[9u], m[4u]);
-
- // Round 3
- G(0u, 4u, 8u, 12u, m[7u], m[9u], 1u, 5u, 9u, 13u, m[3u], m[1u]);
- G(2u, 6u, 10u, 14u, m[13u], m[12u], 3u, 7u, 11u, 15u, m[11u], m[14u]);
- G(0u, 5u, 10u, 15u, m[2u], m[6u], 1u, 6u, 11u, 12u, m[5u], m[10u]);
- G(2u, 7u, 8u, 13u, m[4u], m[0u], 3u, 4u, 9u, 14u, m[15u], m[8u]);
-
- // Round 4
- G(0u, 4u, 8u, 12u, m[9u], m[0u], 1u, 5u, 9u, 13u, m[5u], m[7u]);
- G(2u, 6u, 10u, 14u, m[2u], m[4u], 3u, 7u, 11u, 15u, m[10u], m[15u]);
- G(0u, 5u, 10u, 15u, m[14u], m[1u], 1u, 6u, 11u, 12u, m[11u], m[12u]);
- G(2u, 7u, 8u, 13u, m[6u], m[8u], 3u, 4u, 9u, 14u, m[3u], m[13u]);
-
- // Round 5
- G(0u, 4u, 8u, 12u, m[2u], m[12u], 1u, 5u, 9u, 13u, m[6u], m[10u]);
- G(2u, 6u, 10u, 14u, m[0u], m[11u], 3u, 7u, 11u, 15u, m[8u], m[3u]);
- G(0u, 5u, 10u, 15u, m[4u], m[13u], 1u, 6u, 11u, 12u, m[7u], m[5u]);
- G(2u, 7u, 8u, 13u, m[15u], m[14u], 3u, 4u, 9u, 14u, m[1u], m[9u]);
-
- // Round 6
- G(0u, 4u, 8u, 12u, m[12u], m[5u], 1u, 5u, 9u, 13u, m[1u], m[15u]);
- G(2u, 6u, 10u, 14u, m[14u], m[13u], 3u, 7u, 11u, 15u, m[4u], m[10u]);
- G(0u, 5u, 10u, 15u, m[0u], m[7u], 1u, 6u, 11u, 12u, m[6u], m[3u]);
- G(2u, 7u, 8u, 13u, m[9u], m[2u], 3u, 4u, 9u, 14u, m[8u], m[11u]);
-
- // Round 7
- G(0u, 4u, 8u, 12u, m[13u], m[11u], 1u, 5u, 9u, 13u, m[7u], m[14u]);
- G(2u, 6u, 10u, 14u, m[12u], m[1u], 3u, 7u, 11u, 15u, m[3u], m[9u]);
- G(0u, 5u, 10u, 15u, m[5u], m[0u], 1u, 6u, 11u, 12u, m[15u], m[4u]);
- G(2u, 7u, 8u, 13u, m[8u], m[6u], 3u, 4u, 9u, 14u, m[2u], m[10u]);
-
- // Round 8
- G(0u, 4u, 8u, 12u, m[6u], m[15u], 1u, 5u, 9u, 13u, m[14u], m[9u]);
- G(2u, 6u, 10u, 14u, m[11u], m[3u], 3u, 7u, 11u, 15u, m[0u], m[8u]);
- G(0u, 5u, 10u, 15u, m[12u], m[2u], 1u, 6u, 11u, 12u, m[13u], m[7u]);
- G(2u, 7u, 8u, 13u, m[1u], m[4u], 3u, 4u, 9u, 14u, m[10u], m[5u]);
-
- // Round 9
- G(0u, 4u, 8u, 12u, m[10u], m[2u], 1u, 5u, 9u, 13u, m[8u], m[4u]);
- G(2u, 6u, 10u, 14u, m[7u], m[6u], 3u, 7u, 11u, 15u, m[1u], m[5u]);
- G(0u, 5u, 10u, 15u, m[15u], m[11u], 1u, 6u, 11u, 12u, m[9u], m[14u]);
- G(2u, 7u, 8u, 13u, m[3u], m[12u], 3u, 4u, 9u, 14u, m[13u], m[0u]);
-
- // Round 10
- G(0u, 4u, 8u, 12u, m[0u], m[1u], 1u, 5u, 9u, 13u, m[2u], m[3u]);
- G(2u, 6u, 10u, 14u, m[4u], m[5u], 3u, 7u, 11u, 15u, m[6u], m[7u]);
- G(0u, 5u, 10u, 15u, m[8u], m[9u], 1u, 6u, 11u, 12u, m[10u], m[11u]);
- G(2u, 7u, 8u, 13u, m[12u], m[13u], 3u, 4u, 9u, 14u, m[14u], m[15u]);
-
- // Round 11
- G(0u, 4u, 8u, 12u, m[14u], m[10u], 1u, 5u, 9u, 13u, m[4u], m[8u]);
- G(2u, 6u, 10u, 14u, m[9u], m[15u], 3u, 7u, 11u, 15u, m[13u], m[6u]);
- G(0u, 5u, 10u, 15u, m[1u], m[12u], 1u, 6u, 11u, 12u, m[0u], m[2u]);
- G(2u, 7u, 8u, 13u, m[11u], m[7u], 3u, 4u, 9u, 14u, m[5u], m[3u]);
-
- // Pixel data set from work seed values
- uvec2 result = BLAKE2B_INIT[0u] ^ v[0u] ^ v[8u];
- if ((validate && uvec2(gl_FragCoord) == uvec2(0u)) || (result.y > difficulty.y || (result.y == difficulty.y && result.x > difficulty.x))) {
- nonce = uvec4((BLAKE2B_INIT[0u] ^ v[0u] ^ v[8u]), m[0u]).yxwz;
- }
-
- // Valid nonce not found
- if (nonce.x == 0u) {
- discard;
- }
-}
+++ /dev/null
-//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
-//! SPDX-FileContributor: Ben Green <ben@latenightsketches.com>
-//! SPDX-License-Identifier: GPL-3.0-or-later AND MIT
-
-import { default as NanoPowGlDownsampleShader } from './gl-downsample.frag'
-import { default as NanoPowGlDrawShader } from './gl-draw.frag'
-import { default as NanoPowGlVertexShader } from './gl-vertex.vert'
-import { isNotHex } from '#utils'
-import type { FBO, NanoPowOptions, WorkGenerateResponse, WorkValidateResponse } from '#types'
-
-/**
-* Nano proof-of-work using WebGL 2.0.
-*/
-export class NanoPowGl {
- static #SEND: bigint = 0xfffffff800000000n
- static #RECEIVE: bigint = 0xfffffe0000000000n
-
- static #isInitialized: boolean = false
- static #busy: boolean = false
- static #debug: boolean = false
- static #raf: number = 0
- /** Used to set canvas size. */
- static #cores: number = Math.max(1, Math.floor(navigator.hardwareConcurrency))
- static #WORKLOAD: number = 256 * this.#cores
- static #canvas: OffscreenCanvas
-
- static #gl: WebGL2RenderingContext | null
- static #drawProgram: WebGLProgram | null
- static #downsampleProgram: WebGLProgram | null
- static #vertexShader: WebGLShader | null
- static #drawShader: WebGLShader | null
- static #downsampleShader: WebGLShader | null
- static #positionBuffer: WebGLBuffer | null
- static #drawFbo: FBO | null
- static #downsampleFbos: FBO[] = []
- static #downsampleSrcLocation: WebGLUniformLocation | null
- static #uboBuffer: WebGLBuffer | null
- static #uboView: DataView = new DataView(new ArrayBuffer(144))
- static #seedBuffer: WebGLBuffer | null
- static #seed: BigUint64Array = new BigUint64Array(1)
- static #query: WebGLQuery | null
- static #pixels: Uint32Array
-
- /** Vertex positions for fullscreen quad. */
- static #positions: Float32Array = new Float32Array([
- -1, -1, 1, -1, 1, 1, -1, 1
- ])
-
- /**
- * Constructs canvas, gets WebGL context, initializes buffers, and compiles
- * shaders.
- */
- static async init (): Promise<void> {
- if (this.#busy) return
- this.#busy = true
-
- try {
- this.#canvas = new OffscreenCanvas(this.#WORKLOAD, this.#WORKLOAD)
- this.#canvas.addEventListener('webglcontextlost', event => {
- event.preventDefault()
- console.warn('WebGL context lost. Waiting for it to be restored...')
- cancelAnimationFrame(this.#raf)
- }, false)
- this.#canvas.addEventListener('webglcontextrestored', event => {
- console.warn('WebGL context restored. Reinitializing...')
- NanoPowGl.init()
- }, false)
- this.#gl = this.#canvas.getContext('webgl2')
- if (this.#gl == null) throw new Error('WebGL 2 is required')
-
- /** Create drawing program */
- this.#drawProgram = this.#gl.createProgram()
- if (this.#drawProgram == null) throw new Error('Failed to create shader program')
-
- this.#vertexShader = this.#gl.createShader(this.#gl.VERTEX_SHADER)
- if (this.#vertexShader == null) throw new Error('Failed to create vertex shader')
- this.#gl.shaderSource(this.#vertexShader, NanoPowGlVertexShader)
- this.#gl.compileShader(this.#vertexShader)
- if (!this.#gl.getShaderParameter(this.#vertexShader, this.#gl.COMPILE_STATUS))
- throw new Error(this.#gl.getShaderInfoLog(this.#vertexShader) ?? `Failed to compile vertex shader`)
-
- this.#drawShader = this.#gl.createShader(this.#gl.FRAGMENT_SHADER)
- if (this.#drawShader == null) throw new Error('Failed to create fragment shader')
- this.#gl.shaderSource(this.#drawShader, NanoPowGlDrawShader)
- this.#gl.compileShader(this.#drawShader)
- if (!this.#gl.getShaderParameter(this.#drawShader, this.#gl.COMPILE_STATUS))
- throw new Error(this.#gl.getShaderInfoLog(this.#drawShader) ?? `Failed to compile fragment shader`)
-
- this.#gl.attachShader(this.#drawProgram, this.#vertexShader)
- this.#gl.attachShader(this.#drawProgram, this.#drawShader)
- this.#gl.linkProgram(this.#drawProgram)
- if (!this.#gl.getProgramParameter(this.#drawProgram, this.#gl.LINK_STATUS))
- throw new Error(this.#gl.getProgramInfoLog(this.#drawProgram) ?? `Failed to link program`)
-
- /** Create downsampling program */
- this.#downsampleProgram = this.#gl.createProgram()
- if (this.#downsampleProgram == null) throw new Error('Failed to create downsample program')
-
- this.#downsampleShader = this.#gl.createShader(this.#gl.FRAGMENT_SHADER)
- if (this.#downsampleShader == null) throw new Error('Failed to create downsample shader')
- this.#gl.shaderSource(this.#downsampleShader, NanoPowGlDownsampleShader)
- this.#gl.compileShader(this.#downsampleShader)
- if (!this.#gl.getShaderParameter(this.#downsampleShader, this.#gl.COMPILE_STATUS))
- throw new Error(this.#gl.getShaderInfoLog(this.#downsampleShader) ?? `Failed to compile downsample shader`)
-
- this.#gl.attachShader(this.#downsampleProgram, this.#vertexShader)
- this.#gl.attachShader(this.#downsampleProgram, this.#downsampleShader)
- this.#gl.linkProgram(this.#downsampleProgram)
- if (!this.#gl.getProgramParameter(this.#downsampleProgram, this.#gl.LINK_STATUS))
- throw new Error(this.#gl.getProgramInfoLog(this.#downsampleProgram) ?? `Failed to link program`)
-
- /** Construct fullscreen quad for rendering */
- this.#gl.useProgram(this.#drawProgram)
- const triangleArray = this.#gl.createVertexArray()
- this.#gl.bindVertexArray(triangleArray)
-
- this.#positionBuffer = this.#gl.createBuffer()
- this.#gl.bindBuffer(this.#gl.ARRAY_BUFFER, this.#positionBuffer)
- this.#gl.bufferData(this.#gl.ARRAY_BUFFER, this.#positions, this.#gl.STATIC_DRAW)
- this.#gl.vertexAttribPointer(0, 2, this.#gl.FLOAT, false, 0, 0)
- this.#gl.enableVertexAttribArray(0)
- this.#gl.bindBuffer(this.#gl.ARRAY_BUFFER, null)
-
- /** Create texture and framebuffer for drawing */
- const texture = this.#gl.createTexture()
- this.#gl.bindTexture(this.#gl.TEXTURE_2D, texture)
- this.#gl.texImage2D(this.#gl.TEXTURE_2D, 0, this.#gl.RGBA32UI, this.#gl.drawingBufferWidth, this.#gl.drawingBufferHeight, 0, this.#gl.RGBA_INTEGER, this.#gl.UNSIGNED_INT, null)
- this.#gl.texParameteri(this.#gl.TEXTURE_2D, this.#gl.TEXTURE_MIN_FILTER, this.#gl.NEAREST)
- this.#gl.texParameteri(this.#gl.TEXTURE_2D, this.#gl.TEXTURE_MAG_FILTER, this.#gl.NEAREST)
-
- const framebuffer = this.#gl.createFramebuffer()
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, framebuffer)
- this.#gl.framebufferTexture2D(this.#gl.FRAMEBUFFER, this.#gl.COLOR_ATTACHMENT0, this.#gl.TEXTURE_2D, texture, 0)
- if (this.#gl.checkFramebufferStatus(this.#gl.FRAMEBUFFER) !== this.#gl.FRAMEBUFFER_COMPLETE)
- throw new Error(`Failed to create drawing framebuffer`)
- this.#drawFbo = { texture, framebuffer, size: { x: this.#gl.drawingBufferWidth, y: this.#gl.drawingBufferHeight } }
-
- /** Create textures, framebuffers, and uniform location for downsampling */
- for (let i = 1; i <= 4; i++) {
- const width = this.#gl.drawingBufferWidth / (2 ** i)
- const height = this.#gl.drawingBufferHeight / (2 ** i)
-
- const texture = this.#gl.createTexture()
- this.#gl.bindTexture(this.#gl.TEXTURE_2D, texture)
- this.#gl.texImage2D(this.#gl.TEXTURE_2D, 0, this.#gl.RGBA32UI, width, height, 0, this.#gl.RGBA_INTEGER, this.#gl.UNSIGNED_INT, null)
- this.#gl.texParameteri(this.#gl.TEXTURE_2D, this.#gl.TEXTURE_MIN_FILTER, this.#gl.NEAREST)
- this.#gl.texParameteri(this.#gl.TEXTURE_2D, this.#gl.TEXTURE_MAG_FILTER, this.#gl.NEAREST)
-
- const framebuffer = this.#gl.createFramebuffer()
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, framebuffer)
- this.#gl.framebufferTexture2D(this.#gl.FRAMEBUFFER, this.#gl.COLOR_ATTACHMENT0, this.#gl.TEXTURE_2D, texture, 0)
- if (this.#gl.checkFramebufferStatus(this.#gl.FRAMEBUFFER) !== this.#gl.FRAMEBUFFER_COMPLETE)
- throw new Error(`Failed to create downsampling framebuffer ${i}`)
- this.#downsampleFbos.push({ texture, framebuffer, size: { x: width, y: height } })
- }
- this.#downsampleSrcLocation = this.#gl.getUniformLocation(this.#downsampleProgram, 'src')
- this.#gl.bindTexture(this.#gl.TEXTURE_2D, null)
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, null)
-
- /** Create input buffers */
- this.#uboBuffer = this.#gl.createBuffer()
- this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#uboBuffer)
- this.#gl.bufferData(this.#gl.UNIFORM_BUFFER, 144, this.#gl.DYNAMIC_DRAW)
- this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null)
- this.#gl.bindBufferBase(this.#gl.UNIFORM_BUFFER, 0, this.#uboBuffer)
- this.#gl.uniformBlockBinding(this.#drawProgram, this.#gl.getUniformBlockIndex(this.#drawProgram, 'UBO'), 0)
- this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null)
-
- this.#seedBuffer = this.#gl.createBuffer()
- this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#seedBuffer)
- this.#gl.bufferData(this.#gl.UNIFORM_BUFFER, 16, this.#gl.DYNAMIC_DRAW)
- this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null)
- this.#gl.bindBufferBase(this.#gl.UNIFORM_BUFFER, 1, this.#seedBuffer)
- this.#gl.uniformBlockBinding(this.#drawProgram, this.#gl.getUniformBlockIndex(this.#drawProgram, 'WORK'), 1)
- this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null)
-
- /** Finalize configuration */
- this.#query = this.#gl.createQuery()
- this.#pixels = new Uint32Array(this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight * 4)
- console.log(`NanoPow WebGL initialized. Maximum nonces checked per frame: ${this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight}`)
- this.#isInitialized = true
- } catch (err) {
- throw new Error('WebGL initialization failed.', { cause: err })
- } finally {
- this.#busy = false
- }
- }
-
- /**
- * On WebGL context loss, attempts to clear all program variables and then
- * reinitialize them by calling `init()`.
- */
- static reset (): void {
- cancelAnimationFrame(NanoPowGl.#raf)
- NanoPowGl.#gl?.deleteQuery(NanoPowGl.#query)
- NanoPowGl.#query = null
- NanoPowGl.#gl?.deleteBuffer(NanoPowGl.#seedBuffer)
- NanoPowGl.#seedBuffer = null
- NanoPowGl.#gl?.deleteBuffer(NanoPowGl.#uboBuffer)
- NanoPowGl.#uboBuffer = null
- for (const fbo of NanoPowGl.#downsampleFbos) {
- NanoPowGl.#gl?.deleteFramebuffer(fbo.framebuffer)
- NanoPowGl.#gl?.deleteTexture(fbo.texture)
- }
- NanoPowGl.#downsampleFbos = []
- NanoPowGl.#gl?.deleteShader(NanoPowGl.#downsampleShader)
- NanoPowGl.#downsampleShader = null
- NanoPowGl.#gl?.deleteProgram(NanoPowGl.#downsampleProgram)
- NanoPowGl.#downsampleProgram = null
- NanoPowGl.#gl?.deleteFramebuffer(NanoPowGl.#drawFbo?.framebuffer ?? null)
- NanoPowGl.#drawFbo = null
- NanoPowGl.#gl?.deleteTexture(NanoPowGl.#drawFbo)
- NanoPowGl.#drawFbo = null
- NanoPowGl.#gl?.deleteBuffer(NanoPowGl.#positionBuffer)
- NanoPowGl.#positionBuffer = null
- NanoPowGl.#gl?.deleteShader(NanoPowGl.#drawShader)
- NanoPowGl.#drawShader = null
- NanoPowGl.#gl?.deleteShader(NanoPowGl.#vertexShader)
- NanoPowGl.#vertexShader = null
- NanoPowGl.#gl?.deleteProgram(NanoPowGl.#drawProgram)
- NanoPowGl.#drawProgram = null
- NanoPowGl.#gl = null
- NanoPowGl.#busy = false
- NanoPowGl.#isInitialized = false
- NanoPowGl.init()
- }
-
- static #logAverages (times: number[]): void {
- let count = times.length, sum = 0, reciprocals = 0, logarithms = 0, truncated = 0, min = 0xffff, max = 0, rate = 0
- times.sort()
- for (let i = 0; i < count; i++) {
- sum += times[i]
- reciprocals += 1 / times[i]
- logarithms += Math.log(times[i])
- min = Math.min(min, times[i])
- max = Math.max(max, times[i])
- if (count < 3 || (i > (count * 0.1) && i < (count * 0.9))) truncated += times[i]
- }
- const averages = {
- "Count (frames)": count,
- "Total (ms)": sum,
- "Rate (f/s)": 1000 * count * 0.8 / (truncated || sum),
- "Minimum (ms)": min,
- "Maximum (ms)": max,
- "Arithmetic Mean (ms)": sum / count,
- "Truncated Mean (ms)": truncated / count,
- "Harmonic Mean (ms)": count / reciprocals,
- "Geometric Mean (ms)": Math.exp(logarithms / count)
- }
- console.log(`Averages: ${JSON.stringify(averages)}`)
- console.table(averages)
- }
-
- static #draw (seed: BigUint64Array): void {
- if (this.#gl == null || this.#query == null) throw new Error('WebGL 2 is required to draw and query pixels')
- if (this.#drawFbo == null) throw new Error('FBO is required to draw')
- if (this.#seed[0] == null || this.#seedBuffer == null) throw new Error('Seed is required to draw')
-
- /** Upload work seed buffer */
- this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#seedBuffer)
- this.#gl.bufferSubData(this.#gl.UNIFORM_BUFFER, 0, seed)
- this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null)
-
- /** Draw full canvas */
- this.#gl.useProgram(this.#drawProgram)
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, this.#drawFbo.framebuffer)
- this.#gl.activeTexture(this.#gl.TEXTURE0)
- this.#gl.bindTexture(this.#gl.TEXTURE_2D, this.#drawFbo.texture)
-
- this.#gl.beginQuery(this.#gl.ANY_SAMPLES_PASSED_CONSERVATIVE, this.#query)
- this.#gl.viewport(0, 0, this.#drawFbo.size.x, this.#drawFbo.size.y)
- this.#gl.drawArrays(this.#gl.TRIANGLES, 0, 4)
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, null)
- this.#gl.endQuery(this.#gl.ANY_SAMPLES_PASSED_CONSERVATIVE)
- }
-
- static async #checkQueryResult (): Promise<boolean> {
- return new Promise((resolve, reject) => {
- function check () {
- try {
- if (NanoPowGl.#gl == null || NanoPowGl.#query == null) throw new Error('WebGL 2 is required to check query results')
- if (NanoPowGl.#gl.getQueryParameter(NanoPowGl.#query, NanoPowGl.#gl.QUERY_RESULT_AVAILABLE)) {
- resolve(!!(NanoPowGl.#gl.getQueryParameter(NanoPowGl.#query, NanoPowGl.#gl.QUERY_RESULT)))
- } else {
- /** Query result not yet available, check again in the next frame */
- NanoPowGl.#raf = requestAnimationFrame(check)
- }
- } catch (err) {
- reject(err)
- }
- }
- check()
- })
- }
-
- /**
- * When a result is found by the `gl.query`, downsamples the texture to speed
- * up the subsequent `readPixels` call, reads the pixels into the work buffer,
- * checks every 4th pixel for the 'found' byte, converts the subsequent two
- * pixels with the nonce byte values to a hex string, and returns the result.
- *
- * @param {string} [workHex] - Original nonce if provided for a validation call
- * @returns Nonce as an 8-byte (16-char) hexadecimal string
- */
- static #readResult (workHex?: string): { result: bigint, nonce: bigint } {
- if (this.#gl == null) throw new Error('WebGL 2 is required to read pixels')
- if (this.#drawFbo == null) throw new Error('Source FBO is required to downsample')
-
- let source = this.#drawFbo
- let pixelCount
- const start = performance.now()
- if (workHex != null) {
- /** Read validate results immedidately without unnecessary downsampling */
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, source.framebuffer)
- this.#gl.readPixels(0, 0, 1, 1, this.#gl.RGBA_INTEGER, this.#gl.UNSIGNED_INT, this.#pixels)
- pixelCount = 4
- } else {
- /** Downsample framebuffer */
- this.#gl.useProgram(this.#downsampleProgram)
- for (const fbo of this.#downsampleFbos) {
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, fbo.framebuffer)
- this.#gl.activeTexture(this.#gl.TEXTURE0)
- this.#gl.bindTexture(this.#gl.TEXTURE_2D, source.texture)
- this.#gl.uniform1i(this.#downsampleSrcLocation, 0)
- this.#gl.viewport(0, 0, fbo.size.x, fbo.size.y)
- this.#gl.drawArrays(this.#gl.TRIANGLES, 0, 4)
- source = fbo
- }
- /** Read downsampled result */
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, source.framebuffer)
- this.#gl.readPixels(0, 0, source.size.x, source.size.y, this.#gl.RGBA_INTEGER, this.#gl.UNSIGNED_INT, this.#pixels)
- pixelCount = source.size.x * source.size.y * 4
- }
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, null)
-
- for (let i = 0; i < pixelCount; i += 4) {
- if (this.#pixels[i] !== 0) {
- if (this.#debug) console.log(`readResults (${performance.now() - start} ms)`)
- if (this.#debug) console.log(`Pixel: rgba(${this.#pixels[i]}, ${this.#pixels[i + 1]}, ${this.#pixels[i + 2]}, ${this.#pixels[i + 3]})`)
- /** Return the work value with the custom bits */
- const result = `${this.#pixels[i].toString(16).padStart(8, '0')}${this.#pixels[i + 1].toString(16).padStart(8, '0')}`
- const nonce = `${this.#pixels[i + 2].toString(16).padStart(8, '0')}${this.#pixels[i + 3].toString(16).padStart(8, '0')}`
- if (workHex == null || workHex == nonce) {
- return {
- result: BigInt(`0x${result}`),
- nonce: BigInt(`0x${nonce}`)
- }
- }
- }
- }
- throw new Error('Query reported result but nonce value not found')
- }
-
- /**
- * Finds a nonce that satisfies the Nano proof-of-work requirements.
- *
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
- * @param {NanoPowOptions} options - Options used to configure search execution
- */
- static async work_generate (hash: string, options?: NanoPowOptions): Promise<WorkGenerateResponse> {
- if (isNotHex(hash, 64)) throw new Error(`Invalid hash ${hash}`)
- if (this.#busy) {
- console.log('NanoPowGl is busy. Retrying search...')
- return new Promise(resolve => {
- setTimeout(async (): Promise<void> => {
- const result = this.work_generate(hash, options)
- resolve(result)
- }, 500)
- })
- }
- if (this.#isInitialized === false) this.init()
-
- options ??= {}
- options.debug ??= false
- options.difficulty ??= 0xfffffff800000000n
- options.effort ??= 0x4
-
- if (typeof options?.difficulty !== 'string'
- && typeof options?.difficulty !== 'bigint'
- ) {
- throw new TypeError(`Invalid difficulty ${options?.difficulty}`)
- }
- const difficulty = typeof options.difficulty === 'string'
- ? BigInt(`0x${options.difficulty}`)
- : options.difficulty
-
- if (difficulty < 0x0n || difficulty > 0xfffffff800000000n) {
- throw new TypeError(`Invalid difficulty ${options.difficulty}`)
- }
-
- const effort = (typeof options?.effort !== 'number' || options.effort < 0x1 || options.effort > 0x20)
- ? this.#cores
- : options.effort
-
- this.#busy = true
-
- this.#debug = !!(options?.debug)
-
- if (this.#debug) console.log('NanoPowGl.work_generate()')
- if (this.#debug) console.log('blockhash', hash)
- if (this.#debug) console.log('search options', JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
-
- /** Reset if user specified new level of effort */
- if (this.#WORKLOAD !== 256 * effort) {
- this.#WORKLOAD = 256 * effort
- this.#canvas.height = this.#WORKLOAD
- this.#canvas.width = this.#WORKLOAD
- this.reset()
- }
- if (NanoPowGl.#gl == null) throw new Error('WebGL 2 is required')
- if (this.#gl == null) throw new Error('WebGL 2 is required')
- if (this.#drawFbo == null) throw new Error('WebGL framebuffer is required')
-
- /** Clear any previous results */
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, this.#drawFbo.framebuffer)
- this.#gl.clearBufferuiv(this.#gl.COLOR, 0, [0, 0, 0, 0])
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, null)
-
- /** Set up uniform buffer object */
- for (let i = 0; i < this.#uboView.byteLength; i++) this.#uboView.setUint8(i, 0)
- for (let i = 0; i < 64; i += 8) {
- const uint32 = hash.slice(i, i + 8)
- this.#uboView.setUint32(i * 2, parseInt(uint32, 16))
- }
- this.#uboView.setBigUint64(128, difficulty, true)
- this.#uboView.setUint32(136, 0, true)
- if (this.#debug) console.log('UBO', this.#uboView.buffer.slice(0))
- this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#uboBuffer)
- this.#gl.bufferSubData(this.#gl.UNIFORM_BUFFER, 0, this.#uboView)
- this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null)
-
- /** Start drawing to calculate one nonce per pixel */
- let times = []
- let start = performance.now()
- let result = 0n
- let nonce = 0n
- let found = false
-
- if (this.#debug) console.groupCollapsed('Seeds (click to view)')
- while (!found) {
- start = performance.now()
- const random0 = Math.floor(Math.random() * 0xffffffff)
- const random1 = Math.floor(Math.random() * 0xffffffff)
- this.#seed[0] = (BigInt(random0) << 32n) | BigInt(random1)
- if (this.#debug) console.log('Seed', this.#seed)
- this.#draw(this.#seed)
- found = await this.#checkQueryResult()
- times.push(performance.now() - start)
- if (found) {
- ({ result, nonce } = this.#readResult())
- if (this.#debug) console.groupEnd()
- }
- }
- this.#busy = false
- if (this.#debug) this.#logAverages(times)
- return {
- hash,
- work: nonce.toString(16).padStart(16, '0'),
- difficulty: result.toString(16).padStart(16, '0')
- }
- }
-
- /**
- * Validates that a nonce satisfies Nano proof-of-work requirements.
- *
- * @param {string} work - Hexadecimal proof-of-work value to validate
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
- * @param {NanoPowOptions} options - Options used to configure search execution
- */
- static async work_validate (work: string, hash: string, options?: NanoPowOptions): Promise<WorkValidateResponse> {
- if (isNotHex(work, 16)) throw new Error(`Invalid work ${work}`)
- if (isNotHex(hash, 64)) throw new Error(`Invalid hash ${hash}`)
- if (this.#busy) {
- console.log('NanoPowGl is busy. Retrying validate...')
- return new Promise(resolve => {
- setTimeout(async (): Promise<void> => {
- const result = this.work_validate(work, hash, options)
- resolve(result)
- }, 500)
- })
- }
- if (this.#isInitialized === false) this.init()
-
- options ??= {}
- options.debug ??= false
- options.difficulty ??= 0xfffffff800000000n
- options.effort ??= 0x4
-
- if (typeof options?.difficulty !== 'string'
- && typeof options?.difficulty !== 'bigint'
- ) {
- throw new TypeError(`Invalid difficulty ${options?.difficulty}`)
- }
- const difficulty = typeof options.difficulty === 'string'
- ? BigInt(`0x${options.difficulty}`)
- : options.difficulty
-
- if (difficulty < 0x0n || difficulty > 0xfffffff800000000n) {
- throw new TypeError(`Invalid difficulty ${options.difficulty}`)
- }
-
- this.#busy = true
-
- this.#debug = !!(options?.debug)
-
- if (this.#debug) console.log('NanoPowGl.work_validate()')
- if (this.#debug) console.log('blockhash', hash)
- if (this.#debug) console.log('validate options', JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
-
- if (NanoPowGl.#gl == null) throw new Error('WebGL 2 is required')
- if (this.#gl == null) throw new Error('WebGL 2 is required')
- if (this.#drawFbo == null) throw new Error('WebGL framebuffer is required')
-
- /** Clear any previous results */
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, this.#drawFbo.framebuffer)
- this.#gl.clearBufferuiv(this.#gl.COLOR, 0, [0, 0, 0, 0])
- this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, null)
-
- /** Set up uniform buffer object */
- for (let i = 0; i < this.#uboView.byteLength; i++) this.#uboView.setUint8(i, 0)
- for (let i = 0; i < 64; i += 8) {
- const uint32 = hash.slice(i, i + 8)
- this.#uboView.setUint32(i * 2, parseInt(uint32, 16))
- }
- this.#uboView.setBigUint64(128, difficulty, true)
- this.#uboView.setUint32(136, 1, true)
- if (this.#debug) console.log('UBO', this.#uboView.buffer.slice(0))
- this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#uboBuffer)
- this.#gl.bufferSubData(this.#gl.UNIFORM_BUFFER, 0, this.#uboView)
- this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null)
-
- /** Start drawing to calculate one nonce per pixel */
- let result = 0n
- let nonce = 0n
- this.#seed[0] = BigInt(`0x${work}`)
- if (this.#debug) console.log('Work', this.#seed)
- this.#draw(this.#seed)
- let found = await this.#checkQueryResult()
- if (found) {
- try {
- ({ result, nonce } = this.#readResult(work))
- } catch (err) {
- throw new Error('Error reading results', { cause: err })
- }
- } else {
- throw new Error('Failed to find nonce on canvas')
- }
- this.#busy = false
- if (this.#debug) console.log('result', result, result.toString(16).padStart(16, '0'))
- if (this.#debug) console.log('nonce', nonce, nonce.toString(16).padStart(16, '0'))
- const response: WorkValidateResponse = {
- hash,
- work: nonce.toString(16).padStart(16, '0'),
- difficulty: result.toString(16).padStart(16, '0'),
- valid_all: (result >= this.#SEND) ? '1' : '0',
- valid_receive: (result >= this.#RECEIVE) ? '1' : '0',
- }
- if (options?.difficulty != null) response.valid = (result >= difficulty) ? '1' : '0'
- return response
- }
-}
+++ /dev/null
-//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
-//! SPDX-License-Identifier: GPL-3.0-or-later
-
-/**
-* Input buffers
-*/
-struct UBO {
- blockhash: array<vec4<u32>, 2>,
- difficulty: vec2<u32>,
- validate: u32,
- seed: vec2<u32>
-};
-@group(0) @binding(0) var<uniform> ubo: UBO;
-
-/**
-* Output buffers
-*/
-struct WORK {
- found: atomic<u32>,
- nonce: vec2<u32>,
- result: vec2<u32>
-};
-@group(0) @binding(1) var<storage, read_write> work: WORK;
-
-/**
-* Initialization vector defined by BLAKE2. Each vec2<u32> represents two halves
-* of the original u64 value from the reference implementation. They appear
-* reversed pairwise as defined below, but this is an illusion due to endianness:
-* the `x` component of the vector is the low bits and the `y` component is the
-* high bits, and if you laid the bits out individually, they would match the
-* little-endian 64-bit representation.
-*/
-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)
-);
-
-/**
-* Parameter block as defined in BLAKE2 section 2.8 and configured as follows:
-* maximal depth = 1, fanout = 1, digest byte length = 8
-*/
-const BLAKE2B_PARAM = vec2<u32>(0x01010008u, 0u);
-
-/**
-* Message input length which is always 40 for Nano.
-* 8 nonce bytes + 32 block hash bytes
-*/
-const BLAKE2B_INLEN = vec2<u32>(0x00000028u, 0u);
-
-/**
-* Finalization flag as defined in BLAKE2 section 2.4 and set to ~0 since this is
-* the final (and only) message block being hashed.
-*/
-const BLAKE2B_FINAL = vec2<u32>(0xFFFFFFFFu, 0xFFFFFFFFu);
-
-/**
-* Fully initialized state array that is locally copied at each thread start.
-* Application of each XOR is defined by BLAKE2 section 2.4 compression function.
-*/
-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));
-}
-
-/**
-* Used to fill partial `m` vec4 constructions.
-*/
-const Z = vec2(0u);
-
-/**
-* Shared flag to prevent execution for all workgroup threads based on the
-* atomicLoad() result of a single member thread.
-*/
-var<workgroup> validate: bool;
-
-/**
-* Shared flag to prevent execution for all workgroup threads based on the
-* atomicLoad() result of a single member thread.
-*/
-var<workgroup> found: bool;
-
-/**
-* Shared memory for seed and blockhash which do not change during execution and
-* are eventually concatenated to form the hash input message.
-*/
-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>;
-
-/**
-* Main compute function
-*
-* Computes with a workgroup size of 64 which balances warps between NVIDIA and
-* AMD cards while still considering the power-sensitive requirements of mobile
-* devices. The entire workgroup exits immediately if a nonce was already found
-* by a previous workgroup.
-*
-* Each component of a random 8-byte value, provided by the UBO as a vec2<u32>,
-* is XOR'd with a different dimensional index from the global thread identifier
-* to create a unique nonce value for each thread.
-*
-* Where the reference implementation uses array lookups, the NanoPow
-* implementation assigns each array element to its own variable to enhance
-* performance, but the variable name still contains the original index digit.
-*/
-@compute @workgroup_size(64)
-fn main(@builtin(global_invocation_id) global_id: vec3<u32>, @builtin(local_invocation_id) local_id: vec3<u32>) {
- if (local_id.x == 0u) {
- validate = ubo.validate == 1u;
- 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; }
-
- /**
- * Initialize unique nonce
- */
- let m0: vec2<u32> = seed ^ global_id.xy;
-
- /**
- * Compression buffer copied from the modified initialization vector.
- */
- 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]);
-
- /**
- * Twelve rounds of G mixing as part of BLAKE2b compression step, each divided
- * into eight subprocesses, each of which is further paired to be processed in
- * parallel by packing independent vec2 variables into vec4 variables.
- * Each subprocess statement execution is alternated so that the compiler can
- * interleave independent instructions for improved scheduling. That is to say,
- * the first statement `a = a + b` is executed for each subprocess, and then
- * the next statement `a = a + m[sigma[r][2*i+0]]` is executed, and so on
- * through all the steps of the G mix function. Once subprocesses 1-4 are done,
- * computation on subprocesses 5-8 are executed in the same manner.
- *
- * Each subprocess applies transformations to `m` and `v` variables based on a
- * defined set of index inputs. The algorithm for each subprocess is defined as
- * follows:
- *
- * r is the current round
- * i is the current subprocess within that round
- * a, b, c, d are elements of `v` at specific indexes
- * sigma is a defined set of array indexes for `m`
- * rotr64 is a right-hand bit rotation function
- *
- * a = a + b
- * a = a + m[sigma[r][2*i+0]]
- * d = rotr64(d ^ a, 32)
- * c = c + d
- * b = rotr64(b ^ c, 24)
- * a = a + b
- * a = a + m[sigma[r][2*i+1]]
- * d = rotr64(d ^ a, 16)
- * c = c + d
- * b = rotr64(b ^ c, 63)
- *
- * Each sum step has an extra carry addition. Note that the m[sigma] sum is
- * skipped if m[sigma] is zero since it effectively does nothing. Also note
- * that rotations must be applied differently from the reference implementation
- * due to the lack of both a native rotate function and 64-bit support in WGSL.
- */
-
- /**
- * ROUND(0)
- * m[sigma]=(0,1),(2,3),(4,5),(6,7)
- * m[sigma]=(8,9),(10,11),(12,13),(14,15)
- */
- 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);
-
- /**
- * ROUND(1)
- * m[sigma]=(14,10),(4,8),(9,15),(13,6)
- * m[sigma]=(1,12),(0,2),(11,7),(5,3)
- */
- 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);
-
- /**
- * ROUND(2)
- * m[sigma]=(11,8),(12,0),(5,2),(15,13)
- * m[sigma]=(10,14),(3,6),(7,1),(9,4)
- */
- 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);
-
- /**
- * ROUND(3)
- * m[sigma]=(7,9),(3,1),(13,12),(11,14)
- * m[sigma]=(2,6),(5,10),(4,0),(15,8)
- */
- 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);
-
- /**
- * ROUND(4)
- * m[sigma]=(9,0),(5,7),(2,4),(10,15)
- * m[sigma]=(14,1),(11,12),(6,8),(3,13)
- */
- 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);
-
- /**
- * ROUND(5)
- * m[sigma]=(2,12),(6,10),(0,11),(8,3)
- * m[sigma]=(4,13),(7,5),(15,14),(1,9)
- */
- 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);
-
- /**
- * ROUND(6)
- * m[sigma]=(12,5),(1,15),(14,13),(4,10)
- * m[sigma]=(0,7),(6,3),(9,2),(8,11)
- */
- 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);
-
- /**
- * ROUND(7)
- * m[sigma]=(13,11),(7,14),(12,1),(3,9)
- * m[sigma]=(5,0),(15,4),(8,6),(2,10)
- */
- 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);
-
- /**
- * ROUND(8)
- * m[sigma]=(6,15),(14,9),(11,3),(0,8)
- * m[sigma]=(12,2),(13,7),(1,4),(10,5)
- */
- 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);
-
- /**
- * ROUND(9)
- * m[sigma]=(10,2),(8,4),(7,6),(1,5)
- * m[sigma]=(15,11),(9,14),(3,12),(13,0)
- */
- 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);
-
- /**
- * ROUND(10)
- * m[sigma]=(0,1),(2,3),(4,5),(6,7)
- * m[sigma]=(8,9),(10,11),(12,13),(14,15)
- */
- 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);
-
- /**
- * ROUND(11)
- * m[sigma]=(14,10),(4,8),(9,15),(13,6)
- * m[sigma]=(1,12),(0,2),(11,7),(5,3)
- */
- 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);
-
- /****************************************************************************
- * NONCE CHECK *
- ****************************************************************************/
-
- /**
- * Set nonce if it passes the difficulty threshold and no other thread has set it.
- */
- var result: vec2<u32> = BLAKE2B_INIT[0u] ^ v0 ^ v8;
- if (select((result.y > ubo.difficulty.y || (result.y == ubo.difficulty.y && result.x >= ubo.difficulty.x)), all(global_id == vec3(0u)), validate)) {
- loop {
- let swap = atomicCompareExchangeWeak(&work.found, 0u, 1u);
- if (swap.exchanged) {
- work.nonce = m0;
- work.result = result;
- break;
- }
- if (swap.old_value != 0u) {
- break;
- }
- }
- }
- return;
-}
+++ /dev/null
-//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
-//! SPDX-License-Identifier: GPL-3.0-or-later
-
-import { default as NanoPowGpuComputeShader } from './compute.wgsl'
-import { isNotHex } from '#utils'
-import type { NanoPowOptions, NanoPowResult, WorkGenerateResponse, WorkValidateResponse } from '#types'
-
-/**
-* Nano proof-of-work using WebGPU.
-*/
-export class NanoPowGpu {
- static #SEND: bigint = 0xfffffff800000000n
- static #RECEIVE: bigint = 0xfffffe0000000000n
-
- // Initialize WebGPU
- static #isInitialized: boolean = false
- static #debug: boolean = false
- static #action: 'work_generate' | 'work_validate' = 'work_generate'
- static #difficulty: bigint = this.#SEND
- static #effort: number = 4
- static #queue: any[] = []
- static #device: GPUDevice | null = null
- static #bufferReset: BigUint64Array = new BigUint64Array(4)
- static #uboArray: BigUint64Array = new BigUint64Array(8)
- static #uboView: DataView
- static #uboBuffer: GPUBuffer
- static #gpuBuffer: GPUBuffer
- static #cpuBuffer: GPUBuffer
- static #bindGroupLayout: GPUBindGroupLayout | null
- static #bindGroup: GPUBindGroup | null
- static #pipeline: GPUComputePipeline | null
- static #resultView: DataView
- static #result: NanoPowResult
-
- // Initialize WebGPU
- static async init (): Promise<void> {
- console.log('Initializing NanoPowGpu.')
- // Request device and adapter
- try {
- if (navigator.gpu == null) throw new Error('WebGPU is not supported in this browser.')
- const adapter = await navigator.gpu.requestAdapter()
- if (adapter == null) throw new Error('WebGPU adapter refused by browser.')
- const device = await adapter.requestDevice()
- if (!(device instanceof GPUDevice)) throw new Error('WebGPU device failed to load.')
- device.lost?.then(this.reset)
- this.#device = device
-
- // Create buffers for writing GPU calculations and reading from Javascript
- this.#uboView = new DataView(this.#uboArray.buffer)
- this.#uboBuffer = this.#device.createBuffer({
- label: 'ubo',
- size: 64,
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
- })
- this.#gpuBuffer = this.#device.createBuffer({
- label: 'gpu',
- size: 32,
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
- })
- this.#cpuBuffer = this.#device.createBuffer({
- label: 'cpu',
- size: 32,
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
- })
-
- // Create binding group data structure and use it later once UBO is known
- this.#bindGroupLayout = this.#device.createBindGroupLayout({
- entries: [
- { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' }, },
- { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' }, },
- ],
- })
- // Create pipeline to connect compute shader to binding layout
- this.#pipeline = this.#device.createComputePipeline({
- layout: this.#device.createPipelineLayout({
- bindGroupLayouts: [this.#bindGroupLayout]
- }),
- compute: {
- entryPoint: 'main',
- module: this.#device.createShaderModule({
- code: NanoPowGpuComputeShader
- })
- }
- })
- // Bind UBO read and GPU write buffers
- this.#bindGroup = this.#device.createBindGroup({
- layout: this.#bindGroupLayout,
- entries: [
- { binding: 0, resource: { buffer: this.#uboBuffer }, },
- { binding: 1, resource: { buffer: this.#gpuBuffer }, },
- ],
- })
- // Compile and cache shader prior to actual dispatch
- const cmd = this.#device.createCommandEncoder()
- cmd.beginComputePass().end()
- this.#device.queue.submit([cmd.finish()])
- await this.#device.queue.onSubmittedWorkDone()
- console.log(`NanoPow WebGPU initialized.`)
- this.#isInitialized = true
- } catch (err) {
- throw new Error('WebGPU initialization failed.', { cause: err })
- }
- }
-
- static reset (): void {
- console.warn(`GPU device lost. Reinitializing...`)
- NanoPowGpu.#cpuBuffer?.destroy()
- NanoPowGpu.#gpuBuffer?.destroy()
- NanoPowGpu.#uboBuffer?.destroy()
- NanoPowGpu.#bindGroupLayout = null
- NanoPowGpu.#bindGroup = null
- NanoPowGpu.#pipeline = null
- NanoPowGpu.#isInitialized = false
- queueMicrotask(() => NanoPowGpu.init().catch(console.log))
- }
-
- /**
- * Validate work, if present, and blockhash.
- * Validate options and normalize its properties.
- */
- static async #work_init (work: string | null, hash: string, options?: NanoPowOptions): Promise<void> {
- if (this.#isInitialized === false) await this.init()
- if (this.#debug) console.log(this.#action)
- if (isNotHex(hash, 64)) throw new TypeError(`Invalid hash ${hash}`)
- if (work != null && isNotHex(work, 16)) throw new TypeError(`Invalid work ${work}`)
- options ??= {}
- options.debug ??= false
- options.difficulty ??= this.#SEND
- options.effort ??= 0x4
-
- if (typeof options.effort !== 'number'
- || options.effort < 0x1
- || options.effort > 0x20
- ) {
- throw new TypeError(`Invalid effort ${options.effort}`)
- }
- this.#effort = this.#action === 'work_generate'
- ? options.effort * 0x100
- : 1
-
- if (typeof options.difficulty !== 'string'
- && typeof options.difficulty !== 'bigint'
- ) {
- throw new TypeError(`Invalid difficulty ${options.difficulty}`)
- }
- this.#difficulty = typeof options.difficulty === 'string'
- ? BigInt(`0x${options.difficulty}`)
- : options.difficulty
- if (this.#difficulty < 0x0n || this.#difficulty > this.#SEND) {
- throw new TypeError(`Invalid difficulty ${options.difficulty}`)
- }
-
- this.#debug = !!options.debug
- if (this.#debug) {
- if (work) console.log('work', work)
- console.log('blockhash', hash)
- console.log(`options`, JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
- }
-
- // Ensure WebGPU is initialized before calculating
- let loads = 0
- while (this.#device == null && loads++ < 20) {
- await new Promise(resolve => {
- setTimeout(resolve, 100)
- })
- }
- if (this.#device == null) {
- throw new Error(`WebGPU device failed to load.`)
- }
- // Reset WORK properties to 0u before each calculation
- this.#device.queue.writeBuffer(this.#gpuBuffer, 0, this.#bufferReset)
- this.#device.queue.writeBuffer(this.#cpuBuffer, 0, this.#bufferReset)
-
- // Write data that will not change per dispatch to uniform buffer object
- // Note: u32 size is 4, but total alignment must be multiple of 16
- this.#uboArray.fill(0n)
- for (let i = 0; i < 64; i += 16) {
- this.#uboView.setBigUint64(i / 2, BigInt(`0x${hash.slice(i, i + 16)}`))
- }
- this.#uboView.setBigUint64(32, this.#difficulty, true)
- this.#uboView.setUint32(40, this.#action === 'work_generate' ? 0 : 1, true)
- this.#device.queue.writeBuffer(this.#uboBuffer, 0, this.#uboView)
-
- this.#result = {
- found: false,
- work: 0n,
- difficulty: 0n
- }
- }
-
- static async #work_dispatch (seed: bigint, hash: string): Promise<void> {
- if (this.#device == null) throw new Error(`WebGPU device failed to load.`)
- if (this.#pipeline == null) throw new Error(`WebGPU pipeline failed to load.`)
- if (this.#debug) console.log('seed', seed.toString(16).padStart(16, '0'))
-
- // Copy seed into UBO
- this.#uboView.setBigUint64(48, seed, true)
- if (this.#debug) console.log('UBO', this.#uboView)
- this.#device.queue.writeBuffer(this.#uboBuffer, 0, this.#uboView)
-
- // Create command encoder to issue commands to GPU and initiate computation
- const commandEncoder = this.#device.createCommandEncoder()
- const passEncoder = commandEncoder.beginComputePass()
-
- // Issue commands and end compute pass structure
- passEncoder.setPipeline(this.#pipeline)
- passEncoder.setBindGroup(0, this.#bindGroup)
- passEncoder.dispatchWorkgroups(this.#effort, this.#effort)
- passEncoder.end()
-
- // Copy 8-byte result, 8-byte nonce, and 4-byte found flag from GPU to CPU
- // for reading
- commandEncoder.copyBufferToBuffer(this.#gpuBuffer, 0, this.#cpuBuffer, 0, 32)
-
- // End computation by passing array of command buffers to command queue for execution
- this.#device.queue.submit([commandEncoder.finish()])
-
- // Read results back to Javascript and then unmap buffer after reading
- try {
- await this.#cpuBuffer.mapAsync(GPUMapMode.READ)
- await this.#device.queue.onSubmittedWorkDone()
- this.#resultView = new DataView(this.#cpuBuffer.getMappedRange().slice(0))
- this.#cpuBuffer.unmap()
- } catch (err) {
- console.warn(`Error getting data from GPU. ${err}`)
- this.#cpuBuffer.unmap()
- this.reset()
- }
- if (this.#debug) console.log('gpuBuffer data', this.#resultView)
- if (this.#resultView == null) throw new Error(`Failed to get data from buffer.`)
-
- this.#result.found = !!this.#resultView.getUint32(0)
- this.#result.work = this.#resultView.getBigUint64(8, true)
- this.#result.difficulty = this.#resultView.getBigUint64(16, true)
-
- if (this.#debug) {
- console.log('nonce', this.#result.work, this.#result.work.toString(16).padStart(16, '0'))
- console.log('result', this.#result.difficulty, this.#result.difficulty.toString(16).padStart(16, '0'))
- }
- }
-
- static #work_process (): void {
- const { task, resolve, reject } = this.#queue.shift() ?? {}
- task?.().then(resolve).catch(reject).finally(() => { this.#work_process() })
- }
-
- static async #work_queue (task: Function): Promise<any> {
- return new Promise((resolve, reject): void => {
- this.#queue.push({ task, resolve, reject })
- this.#work_process()
- })
- }
-
- /**
- * Finds a nonce that satisfies the Nano proof-of-work requirements.
- *
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
- * @param {NanoPowOptions} options - Used to configure search execution
- */
- static async work_generate (hash: string, options?: NanoPowOptions): Promise<WorkGenerateResponse> {
- return this.#work_queue(async (): Promise<WorkGenerateResponse> => {
- this.#action = 'work_generate'
- await this.#work_init(null, hash, options)
-
- let random = BigInt(Math.floor(Math.random() * 0xffffffff))
- let seed = random
- do {
- random = BigInt(Math.floor(Math.random() * 0xffffffff))
- seed = (seed & 0xffffffffn) << 32n | random
- await this.#work_dispatch(seed, hash)
- } while (!this.#result.found)
-
- return {
- hash,
- work: this.#result.work.toString(16).padStart(16, '0'),
- difficulty: this.#result.difficulty.toString(16).padStart(16, '0')
- }
- })
- }
-
- /**
- * Validates that a nonce satisfies Nano proof-of-work requirements.
- *
- * @param {string} work - Hexadecimal proof-of-work value to validate
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
- * @param {NanoPowOptions} options - Options used to configure search execution
- */
- static async work_validate (work: string, hash: string, options?: NanoPowOptions): Promise<WorkValidateResponse> {
- return this.#work_queue(async (): Promise<WorkValidateResponse> => {
- this.#action = 'work_validate'
- await this.#work_init(work, hash, options)
-
- const seed = BigInt(`0x${work}`)
- await this.#work_dispatch(seed, hash)
- if (seed !== this.#result.work) throw new Error('Result does not match work')
-
- const response: WorkValidateResponse = {
- hash,
- work: this.#result.work.toString(16).padStart(16, '0'),
- difficulty: this.#result.difficulty.toString(16).padStart(16, '0'),
- valid_all: (this.#result.difficulty >= this.#SEND) ? '1' : '0',
- valid_receive: (this.#result.difficulty >= this.#RECEIVE) ? '1' : '0'
- }
- if (options?.difficulty != null) {
- response.valid = (this.#result.difficulty >= this.#difficulty) ? '1' : '0'
- }
- return response
- })
- }
-}
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { NanoPowGl } from "./gl"
-import { NanoPowGpu } from "./gpu"
+import { NanoPowValidate } from '#lib/validate'
+import { NanoPowCpu, NanoPowWasm, NanoPowWebgl, NanoPowWebgpu } from '#lib/generate'
+import { WorkErrorResponse, WorkGenerateResponse, WorkValidateResponse } from '#types'
+import { bigintFrom, Queue } from '#utils'
+import { NanoPowConfig } from '#lib/config'
-let isGlSupported, isGpuSupported = false
-try {
- const adapter = await navigator?.gpu?.requestAdapter?.()
- isGpuSupported = (adapter instanceof GPUAdapter)
-} catch (err) {
- console.warn('WebGPU is not supported in this environment.\n', err)
- isGpuSupported = false
-}
-try {
- const gl = new OffscreenCanvas(0, 0)?.getContext?.('webgl2')
- isGlSupported = (gl instanceof WebGL2RenderingContext)
-} catch (err) {
- console.warn('WebGL is not supported in this environment.\n', err)
- isGlSupported = false
+const q = new Queue()
+
+export async function work_generate (hash: unknown, options: unknown): Promise<WorkGenerateResponse | WorkErrorResponse> {
+ return q.add(async (): Promise<WorkGenerateResponse | WorkErrorResponse> => {
+ try {
+ const { api, debug, difficulty, effort } = await NanoPowConfig(options)
+ switch (api) {
+ case 'webgpu': {
+ return NanoPowWebgpu(bigintFrom(hash, 'hex'), difficulty, effort, debug)
+ }
+ case 'webgl': {
+ return NanoPowWebgl(bigintFrom(hash, 'hex'), difficulty, effort, debug)
+ }
+ case 'wasm': {
+ return NanoPowWasm(bigintFrom(hash, 'hex'), difficulty, effort, debug)
+ }
+ default: {
+ return NanoPowCpu(bigintFrom(hash, 'hex'), difficulty, debug)
+ }
+ }
+ } catch (e: any) {
+ return { error: typeof e === 'string' ? e : (e?.message ?? '') }
+ }
+ })
}
-const NanoPow = isGpuSupported ? NanoPowGpu : isGlSupported ? NanoPowGl : null
-export { NanoPow, NanoPowGl, NanoPowGpu }
+export async function work_validate (work: unknown, hash: unknown, options: unknown): Promise<WorkValidateResponse | WorkErrorResponse> {
+ try {
+ const { debug, difficulty } = await NanoPowConfig(options)
+ const result = await NanoPowValidate(bigintFrom(work, 'hex'), bigintFrom(hash, 'hex'), difficulty, debug)
+ return result
+ } catch (e: any) {
+ return { error: (typeof e === 'string' ? e : (e?.message ?? '')) }
+ }
+}
+++ /dev/null
-// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-declare module '*.frag' {
- const value: string
- export default value
-}
-
-declare module '*.glsl' {
- const value: string
- export default value
-}
-
-declare module '*.vert' {
- const value: string
- export default value
-}
-
-declare module '*.wgsl' {
- const value: string
- export default value
-}
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { WorkValidateResponse } from '#types'
+import { bigintAsUintNArray, bigintToHex, Logger, RECEIVE, SEND } from '#utils'
+
+const logger = new Logger()
+
+const blake2b_IV: readonly bigint[] = Object.freeze([
+ 0x6a09e667f3bcc908n,
+ 0xbb67ae8584caa73bn,
+ 0x3c6ef372fe94f82bn,
+ 0xa54ff53a5f1d36f1n,
+ 0x510e527fade682d1n,
+ 0x9b05688c2b3e6c1fn,
+ 0x1f83d9abfb41bd6bn,
+ 0x5be0cd19137e2179n
+])
+
+const blake2b_sigma: readonly (readonly number[])[] = Object.freeze([
+ Object.freeze([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),
+ Object.freeze([14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3]),
+ Object.freeze([11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4]),
+ Object.freeze([7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8]),
+ Object.freeze([9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13]),
+ Object.freeze([2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9]),
+ Object.freeze([12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11]),
+ Object.freeze([13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10]),
+ Object.freeze([6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5]),
+ Object.freeze([10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0]),
+ Object.freeze([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),
+ Object.freeze([14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3])
+])
+
+const blake2b_param = 0x01010008n
+
+// Initialize CPU
+const v: BigUint64Array = new BigUint64Array(16)
+const m: BigUint64Array = new BigUint64Array(16)
+const mView: DataView = new DataView(m.buffer)
+let result: bigint = 0n
+
+function G (a: number, b: number, c: number, d: number, x: number, y: number): void {
+ v[a] += v[b] + m[x]
+ v[d] ^= v[a]
+ v[d] = (v[d] >> 32n) | (v[d] << 32n)
+ v[c] += v[d]
+ v[b] ^= v[c]
+ v[b] = (v[b] >> 24n) | (v[b] << 40n)
+ v[a] += v[b] + m[y]
+ v[d] ^= v[a]
+ v[d] = (v[d] >> 16n) | (v[d] << 48n)
+ v[c] += v[d]
+ v[b] ^= v[c]
+ v[b] = (v[b] >> 63n) | (v[b] << 1n)
+}
+
+function ROUND (i: number): void {
+ const s = blake2b_sigma[i]
+ G(0, 4, 8, 12, s[0], s[1])
+ G(1, 5, 9, 13, s[2], s[3])
+ G(2, 6, 10, 14, s[4], s[5])
+ G(3, 7, 11, 15, s[6], s[7])
+ G(0, 5, 10, 15, s[8], s[9])
+ G(1, 6, 11, 12, s[10], s[11])
+ G(2, 7, 8, 13, s[12], s[13])
+ G(3, 4, 9, 14, s[14], s[15])
+}
+
+function init (seed: bigint, hash: BigUint64Array): void {
+ // Reset buffers before each calculation
+ result = 0n
+ for (let i = 0; i < 8; i++) {
+ v[i] = blake2b_IV[i]
+ v[i + 8] = blake2b_IV[i]
+ }
+ v[0] ^= blake2b_param // depth = 1; fanout = 1; outlen = 8
+ v[12] ^= 40n // Output length
+ v[14] = ~v[14] // Compression
+
+ // Set up input buffers
+ mView.setBigUint64(0, seed, true)
+ for (let i = 0; i < 4; i++) {
+ mView.setBigUint64(8 * (i + 1), hash[i])
+ }
+}
+
+function blake2b (work: bigint, hash: bigint): void {
+ init(work, bigintAsUintNArray(hash, 64, 4))
+ for (let i = 0; i < 12; i++) {
+ ROUND(i)
+ }
+ result = (blake2b_IV[0] ^ 0x01010008n ^ v[0] ^ v[8])
+}
+
+function log (work: bigint, hash: bigint, difficulty: bigint): void {
+ logger.groupStart('NanoPow CPU work_validate')
+ logger.log('NanoPow CPU work_validate', 'work', bigintToHex(work, 16))
+ logger.log('NanoPow CPU work_validate', 'hash', bigintToHex(hash, 64))
+ logger.log('NanoPow CPU work_validate', 'difficulty', bigintToHex(difficulty, 16))
+ logger.log('NanoPow CPU work_validate', 'result', bigintToHex(result, 16))
+ logger.groupEnd('NanoPow CPU work_validate')
+}
+
+function validate (work: bigint, hash: bigint, difficulty: bigint, debug: boolean): WorkValidateResponse {
+ logger.isEnabled = debug
+ blake2b(work, hash)
+ log(work, hash, difficulty)
+ return {
+ hash: bigintToHex(hash, 64),
+ work: bigintToHex(work, 16),
+ difficulty: bigintToHex(result, 16),
+ valid: (result >= difficulty) ? '1' : '0',
+ valid_all: (result >= SEND) ? '1' : '0',
+ valid_receive: (result >= RECEIVE) ? '1' : '0'
+ }
+}
+
+export { validate as NanoPowValidate }
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
//! SPDX-License-Identifier: GPL-3.0-or-later
-export { NanoPow as default, NanoPow, NanoPowGl, NanoPowGpu } from './lib'
+import { work_generate, work_validate } from './lib'
+import { NanoPowOptions, WorkErrorResponse, WorkGenerateResponse, WorkValidateResponse } from '#types'
+
+export class NanoPow {
+ /**
+ * Finds a nonce that satisfies the Nano proof-of-work requirements.
+ *
+ * @param {bigint | string} hash - Hexadecimal hash of previous block, or public key for new accounts
+ * @param {object} [options] - Used to configure execution
+ * @param {string} [options.api] - Specifies how work is generated. Default: best available
+ * @param {boolean} [options.debug=false] - Enables additional debug logging to the console. Default: false
+ * @param {number} [options.effort=0x4] - GPU load when generating work. Larger values are not necessarily better since they can quickly overwhelm the GPU. Default: 0x4
+ * @param {bigint} [options.difficulty=0xfffffff800000000] - Minimum value result of `BLAKE2b(nonce||blockhash)`. Default: 0xFFFFFFF800000000
+ */
+ static async work_generate (hash: bigint | string, options: NanoPowOptions): Promise<WorkGenerateResponse | WorkErrorResponse> {
+ return work_generate(hash, options)
+ }
+
+ /**
+ * Validates that a nonce satisfies Nano proof-of-work requirements.
+ *
+ * @param {(bigint|string)} work - Value to validate against hash and difficulty
+ * @param {(bigint|string)} hash - Hash of previous block, or public key for new accounts
+ * @param {object} [options] - Used to configure execution
+ * @param {boolean} [options.debug=false] - Enables additional debug logging to the console. Default: false
+ * @param {bigint} [options.difficulty=0xfffffff800000000] - Minimum value result of `BLAKE2b(nonce||blockhash)`. Default: 0xFFFFFFF800000000
+ */
+ static async work_validate (work: bigint | string, hash: bigint | string, options: NanoPowOptions): Promise<WorkValidateResponse | WorkErrorResponse> {
+ return work_validate(work, hash, options)
+ }
+}
+
+export { NanoPow as default }
+export { stats } from '#utils'
-// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
-// SPDX-License-Identifier: GPL-3.0-or-later
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
declare global {
interface Window {
}
}
-declare const NanoPow: typeof NanoPowGl | typeof NanoPowGpu | null
-
-/**
-* Nano proof-of-work using WebGL 2.0.
-*/
-export declare class NanoPowGl {
- #private
- /**
- * Constructs canvas, gets WebGL context, initializes buffers, and compiles
- * shaders.
- */
- static init (): Promise<void>
- /**
- * On WebGL context loss, attempts to clear all program variables and then
- * reinitialize them by calling `init()`.
- */
- static reset (): void
- /**
- * Finds a nonce that satisfies the Nano proof-of-work requirements.
- *
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
- * @param {NanoPowOptions} options - Options used to configure search execution
- */
- static work_generate (hash: string, options?: NanoPowOptions): Promise<WorkGenerateResponse>
- /**
- * Validates that a nonce satisfies Nano proof-of-work requirements.
- *
- * @param {string} work - Hexadecimal proof-of-work value to validate
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
- * @param {NanoPowOptions} options - Options used to configure search execution
- */
- static work_validate (work: string, hash: string, options?: NanoPowOptions): Promise<WorkValidateResponse>
+export declare const ApiSupported: {
+ cpu: boolean
+ wasm: {
+ isSupported: boolean
+ }
+ webgl: {
+ isSupported: boolean
+ }
+ webgpu: {
+ isSupported: boolean
+ }
}
+export type ApiSupportedTypes = keyof typeof ApiSupported
-/**
-* Nano proof-of-work using WebGPU.
-*/
-export declare class NanoPowGpu {
- #private
- static init (): Promise<void>
- static reset (): void
+export declare class NanoPow {
/**
* Finds a nonce that satisfies the Nano proof-of-work requirements.
*
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
- * @param {NanoPowOptions} options - Used to configure search execution
+ * @param {bigint | string} hash - Hexadecimal hash of previous block, or public key for new accounts
+ * @param {object} [options] - Used to configure execution
+ * @param {string} [options.api] - Specifies how work is generated. Default: best available
+ * @param {boolean} [options.debug=false] - Enables additional debug logging to the console. Default: false
+ * @param {number} [options.effort=0x4] - GPU load when generating work. Larger values are not necessarily better since they can quickly overwhelm the GPU. Default: 0x4
+ * @param {bigint} [options.difficulty=0xfffffff800000000] - Minimum value result of `BLAKE2b(nonce||blockhash)`. Default: 0xFFFFFFF800000000
*/
- static work_generate (hash: string, options?: NanoPowOptions): Promise<WorkGenerateResponse>
+ static work_generate (hash: bigint | string, options: NanoPowOptions): Promise<WorkGenerateResponse | WorkErrorResponse>
/**
* Validates that a nonce satisfies Nano proof-of-work requirements.
*
- * @param {string} work - Hexadecimal proof-of-work value to validate
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
- * @param {NanoPowOptions} options - Options used to configure search execution
+ * @param {(bigint|string)} work - Value to validate against hash and difficulty
+ * @param {(bigint|string)} hash - Hash of previous block, or public key for new accounts
+ * @param {object} [options] - Used to configure execution
+ * @param {boolean} [options.debug=false] - Enables additional debug logging to the console. Default: false
+ * @param {bigint} [options.difficulty=0xfffffff800000000] - Minimum value result of `BLAKE2b(nonce||blockhash)`. Default: 0xFFFFFFF800000000
*/
- static work_validate (work: string, hash: string, options?: NanoPowOptions): Promise<WorkValidateResponse>
+ static work_validate (work: bigint | string, hash: bigint | string, options: NanoPowOptions): Promise<WorkValidateResponse | WorkErrorResponse>
}
+export { NanoPow as default }
/**
-* Used to configure NanoPow.
+* Input for configuring NanoPow. Must be validated prior to usage, so types are
+* `unknown`, but JSDoc indicates expected data types.
*
+* @param {string} [api=cpu] - Specifies how work is generated. Default: cpu
* @param {boolean} [debug=false] - Enables additional debug logging to the console. Default: false
-* @param {number} [effort=0x8] - Multiplier for dispatching work search. Larger values are not necessarily better since they can quickly overwhelm the GPU. Ignored when validating. Default: 0x8
-* @param {bigint|string} [difficulty=0xfffffff800000000] - Minimum value result of `BLAKE2b(nonce||blockhash)`. Default: 0xFFFFFFF800000000
+* @param {(bigint|string)} [difficulty=0xFFFFFFF800000000] - Minimum value result of `BLAKE2b(nonce||blockhash)`. Default: 0xFFFFFFF800000000
+* @param {number} [effort=0x4] - GPU load when generating work. Larger values are not necessarily better since they can quickly overwhelm the GPU. Default: 0x4
*/
export type NanoPowOptions = {
- debug?: boolean
- effort?: number
- difficulty?: bigint | string
-}
-
-export type NanoPowResult = {
- found: boolean
- work: bigint
- difficulty: bigint
-}
-
-/**
-* Used to create WebGL framebuffer objects.
-*
-* @param {WebGLTexture} texture - Defines storage size
-* @param {WebGLFramebuffer} framebuffer - Holds texture data
-* @param {size} size - 2D lengths of texture
-*/
-export type FBO = {
- texture: WebGLTexture
- framebuffer: WebGLFramebuffer
- size: {
- x: number
- y: number
- }
-}
-
-/**
-* Used to define NanoPow server configuration.
-*
-* @param {boolean} DEBUG - Enables additional log output
-* @param {number} EFFORT - Defines dispatch size per compute pass
-* @param {number} PORT - TCP port on which to listen for requests
-*/
-export type NanoPowServerConfig = {
- [key: string]: boolean | number
- DEBUG: boolean
- EFFORT: number
- PORT: number
+ api: unknown
+ debug: unknown
+ difficulty: unknown
+ effort: unknown
}
-export declare const NanoPowGlDownsampleShader: string
-export declare const NanoPowGlDrawShader: string
-export declare const NanoPowGlVertexShader: string
-export declare const NanoPowGpuComputeShader: any
-
/**
-* Used by work server for inbound requests to `work_generate`.
+* Usee to provide consumer with error information in response to request.
*
-* @param {string} action - Method to call
-* @param {string} hash - Block hash used to generate work
-* @param {string} [difficulty=FFFFFFF800000000] - Minimum threshold for a nonce to be valid
+* @param {string} error - Informative message about error.
*/
-type WorkGenerateRequest = {
- [key: string]: string | undefined
- action: 'work_generate'
- hash: string
- difficulty?: string
+export type WorkErrorResponse = {
+ error: string
}
/**
-* Used by work server for outbound responses to `work_generate`.
+* Structure of response to `work_generate` call.
*
-* @param {string} hash - Block hash used to generate or validate work
+* @param {string} hash - Hash from generate request
* @param {string} work - Valid proof-of-work nonce generated for input hash
-* @param {string} difficulty - BLAKE2b hash result which was compared to specified minimum threshold
+* @param {string} difficulty - BLAKE2b output which met or exceeded specified minimum threshold
*/
-type WorkGenerateResponse = {
- [key: string]: string
+export type WorkGenerateResponse = {
hash: string
work: string
difficulty: string
}
/**
-* Used by work server for inbound requests to `work_validate`.
-*
-* @param {string} action - Method to call
-* @param {string} hash - Block hash used to validate work
-* @param {string} work - Existing nonce to check against hash
-* @param {string} [difficulty=FFFFFFF800000000] - Minimum threshold for a nonce to be valid
-*/
-type WorkValidateRequest = {
- [key: string]: string | undefined
- action: 'work_validate'
- hash: string
- work: string
- difficulty?: string
-}
-
-/**
-* Used by work server for outbound responses to `work_validate`.
+* Structure of response to `work_validate` call.
*
* @param {string} hash - Hash from validate request
* @param {string} work - Nonce from validate request
-* @param {string} difficulty - BLAKE2b hash result which is compared to specified minimum threshold
-* @param {string} [valid] - Excluded if optional difficulty was not included in the request. 1 for true if nonce is valid for requested difficulty, else 0 for false
+* @param {string} difficulty - BLAKE2b output of `work` + `hash` which is compared to specified minimum threshold to determine validity
* @param {string} valid_all - 1 for true if nonce is valid for send blocks, else 0 for false
* @param {string} valid_receive - 1 for true if nonce is valid for receive blocks, else 0 for false
+* @param {string} [valid] - Excluded if optional difficulty was not included in the request. 1 for true if nonce is valid for requested difficulty, else 0 for false
*/
-type WorkValidateResponse = {
- [key: string]: string | undefined
+export type WorkValidateResponse = {
hash: string
work: string
difficulty: string
- valid?: '0' | '1'
valid_all: '0' | '1'
valid_receive: '0' | '1'
+ valid?: '0' | '1'
}
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { wasm } from './wasm'
+import { webgl } from './webgl'
+import { webgpu } from './webgpu'
+
+export const ApiSupport = { cpu: { isSupported: true }, wasm, webgl, webgpu }
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+const wasm = { isSupported: false }
+Object.defineProperty(wasm, 'isSupported', {
+ get: async function () {
+ let isWasmSupported = false
+ try {
+ // (func (result v128) (v128.const i32x4 0 0 0 0))
+ const wasmBuffer = new Uint8Array([
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // WASM magic header and version
+ 0x01, 0x04, // Type section, 4 byte descriptor
+ 0x01, 0x60, 0x00, 0x00, // 1 type, type function, 0 params, 0 returns
+ 0x03, 0x02, // Function section, 2 byte descriptor
+ 0x01, 0x00, // 1 function, ID 0
+ 0x0a, 0x17, // Code section, 23 bytes
+ 0x01, 0x15, // 1 function body, 21 bytes
+ 0x00, // 0 local variables
+ 0xfd, 0x0c, // v128.const
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // assign i64x2 values
+ 0x1a, 0x0b // 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)
+ isWasmSupported = false
+ } finally {
+ delete this.isSupported
+ this.isSupported = isWasmSupported
+ return this.isSupported
+ }
+ }
+})
+
+export { wasm }
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+const 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
+ }
+ }
+})
+
+export { webgl }
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+const 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
+ }
+ }
+})
+
+export { webgpu }
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+export function bigintAsUintNArray (int: bigint, bits: number, length: number = 0) {
+ const UintTypedArray: { [key: number]: any } = {
+ 8: Uint8Array,
+ 16: Uint16Array,
+ 32: Uint32Array,
+ 64: BigUint64Array
+ }
+ if (![8, 16, 32, 64].includes(bits)) throw new Error('Invalid TypedArray UintN subclass')
+ if (int < 0n) int = ~(int - 1n)
+ length = Math.max(length, Math.ceil(bigintByteLength(int) / bits))
+ const mask: bigint = (1n << BigInt(bits)) - 1n
+ const uintArray = new UintTypedArray[bits](length)
+ for (let i = length - 1; i >= 0; i--) {
+ bits === 64
+ ? uintArray[i] = int & mask
+ : uintArray[i] = (Number(int & mask))
+ int >>= BigInt(bits)
+ }
+ return uintArray
+}
+
+export function bigintBitLength (int: bigint): bigint {
+ let bitLength = 1n
+ while (int > 1n || int < -1n) {
+ bitLength++
+ int >>= 1n
+ }
+ return bitLength
+}
+
+export function bigintByteLength (int: bigint): number {
+ let byteLength = 0
+ while (int > 0n || int < -1n) {
+ byteLength++
+ int >>= 8n
+ }
+ return byteLength
+}
+
+export function bigintFrom (value: bigint | boolean | number | string | unknown, type?: 'bin' | 'oct' | 'hex'): bigint {
+ if (typeof value === 'bigint') {
+ return value
+ } else if (typeof value === 'boolean' || typeof value === 'number') {
+ return BigInt(value)
+ } else if (typeof value === 'string') {
+ const v = value.trim().replace(/n$/, '')
+ if (/^0[Bb][01]+$/.test(v)
+ || /^0[Oo][0-7]+$/.test(v)
+ || /^0[Xx][A-Fa-f\d]+$/.test(v)
+ || /^\d+$/.test(v)) {
+ return BigInt(v)
+ }
+ if (type === 'bin' && /^[01]+$/.test(v)) {
+ return BigInt(`0b${v}`)
+ }
+ if (type === 'oct' && /^[0-7]+$/.test(v)) {
+ return BigInt(`0o${v}`)
+ }
+ if (type === 'hex' || /^\d*[A-Fa-f]+\d*$/.test(v)) {
+ return BigInt(`0x${v}`)
+ }
+ }
+ throw new TypeError(`can't convert string to BigInt`)
+}
+
+export function bigintRandom (max: bigint = 0xFFFFFFFFFFFFFFFFn): bigint {
+ if (typeof max !== 'bigint' || max < 1n) {
+ throw new TypeError('Invalid max value')
+ }
+ const randomUint8Array = new Uint8Array(bigintByteLength(max))
+ const mask = (1n << bigintBitLength(max)) - 1n
+ let output = 0n
+ do {
+ output = 0n
+ crypto.getRandomValues(randomUint8Array)
+ output = BigInt(randomUint8Array[0])
+ for (let i = 1; i < randomUint8Array.length; i++) {
+ output <<= 8n
+ output += BigInt(randomUint8Array[i])
+ }
+ output &= mask
+ } while (output > max)
+ return output
+}
+
+export function bigintToHex (int: bigint, length: unknown = 0): string {
+ if (typeof length !== 'number') {
+ throw new TypeError('invalid length')
+ }
+ return int.toString(16).padStart(length, '0')
+}
-export function average (times: number[]) {
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+export * from './api-support'
+export * from './bigint'
+export * from './logger'
+export * from './queue'
+
+export const SEND = 0xfffffff800000000n
+export const RECEIVE: bigint = 0xfffffe0000000000n
+
+export function stats (times: number[]) {
if (times == null || times.length === 0) return {}
- let count = times.length
+ 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, rate, reciprocals, total, truncated, truncatedCount
- logarithms = max = median = rate = reciprocals = total = truncated = truncatedCount = 0
+ let logarithms, max, median, rate, reciprocals, total
+ logarithms = max = median = rate = 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
- reciprocals += 1 / time
logarithms += Math.log(time)
+ reciprocals += 1 / time
min = Math.min(min, time)
max = Math.max(max, time)
- if (i + 1 === Math.ceil(count / 2)) median = time
- if (count < 3 || (i > (0.1 * count) && i < (0.9 * (count - 1)))) {
- truncated += time
- truncatedCount++
- }
+ 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: 1000 * count / total,
min,
max,
median,
arithmetic: total / count,
- harmonic: count / reciprocals,
geometric: Math.exp(logarithms / count),
- truncated: truncated / truncatedCount,
- rate: 1000 * truncatedCount / (truncated || total),
- }
-}
-
-export function isHex (input: string, min?: number, max?: number): boolean {
- if (typeof input !== 'string') {
- return false
- }
- if (typeof min !== 'undefined' && typeof min !== 'number') {
- throw new Error(`Invalid argument for parameter 'min'`)
- }
- if (typeof max !== 'undefined' && typeof max !== 'number') {
- throw new Error(`Invalid argument for parameter 'max'`)
- }
- const range = min === undefined && max === undefined
- ? '+'
- : `{${min ?? '0'},${max ?? ''}}`
- const regexp = new RegExp(`^[0-9A-Fa-f]${range}$`, 'm')
- return regexp.test(input)
-}
-
-export function isNotHex (input: string, min?: number, max?: number): boolean {
- return !isHex(input, min, max)
-}
-
-/**
-* Override console logging to provide an informative prefix for each entry and
-* to only output when debug mode is enabled.
-*/
-export function log (...args: any[]): void {
- if (process?.env?.NANO_POW_DEBUG) {
- const entry = `${new Date(Date.now()).toLocaleString(Intl.DateTimeFormat().resolvedOptions().locale ?? 'en-US', { hour12: false, dateStyle: 'medium', timeStyle: 'medium' })} NanoPow[${process.pid}]: ${args}`
- console.log(entry)
- process.send?.({ type: 'console', message: entry })
+ harmonic: count / reciprocals,
+ truncatedCount,
+ truncatedTotal,
+ truncatedRate: 1000 * truncatedCount / truncatedTotal,
+ truncatedMin,
+ truncatedMax,
+ truncatedArithmetic: truncatedTotal / truncatedCount,
+ truncatedGeometric: Math.exp(truncatedLogarithms / truncatedCount),
+ truncatedHarmonic: truncatedCount / truncatedReciprocals,
}
}
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+/**
+* Override console logging to provide an informative prefix for each entry and
+* to only output when debug mode is enabled.
+*/
+export class Logger {
+ isEnabled: boolean = false
+ groups: { [key: string]: boolean } = {}
+
+ groupStart (name: string): void {
+ if (this.isEnabled) {
+ console.groupCollapsed(name)
+ this.groups[name] = true
+ }
+ }
+
+ groupEnd (name: string): void {
+ if (this.groups[name]) {
+ console.groupEnd()
+ delete this.groups[name]
+ }
+ }
+
+ log (...args: any[]): void {
+ 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}` })
+ }
+ }
+}
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+/**
+ * Serially executes asynchronous functions.
+ */
+export class Queue {
+ #isIdle: boolean
+ #queue: any[]
+
+ constructor () {
+ this.#isIdle = true
+ this.#queue = []
+ }
+
+ #process = (): void => {
+ const { task, resolve, reject, args } = this.#queue.shift() ?? {}
+ this.#isIdle = !task
+ task?.(...args).then(resolve).catch(reject).finally(this.#process)
+ }
+
+ async add (task: unknown, ...args: any[]): Promise<any> {
+ if (typeof task !== 'function') throw new TypeError('task is not a function')
+ return new Promise((resolve, reject): void => {
+ this.#queue.push({ task, resolve, reject, args })
+ if (this.#isIdle) this.#process()
+ })
+ }
+
+ async prioritize (task: unknown, ...args: any[]): Promise<any> {
+ if (typeof task !== 'function') throw new TypeError('task is not a function')
+ return new Promise((resolve, reject): void => {
+ if (typeof task !== 'function') reject('task is not a function')
+ this.#queue.unshift({ task, resolve, reject, args })
+ if (this.#isIdle) this.#process()
+ })
+ }
+}
<head>
<link rel="icon" href="data:,">
<script type="module">
- let NanoPow, NanoPowGl, NanoPowGpu
+ let NanoPow, stats
try {
- ({ NanoPow, NanoPowGl, NanoPowGpu } = await import('../dist/main.min.js'))
+ ({ NanoPow, stats } = await import('../dist/main.min.js'))
} catch (err) {
console.warn(err)
try {
- ({ NanoPow, NanoPowGl, NanoPowGpu } = await import('https://zoso.dev/?p=nano-pow.git;a=blob_plain;f=main.min.js;hb=refs/heads/main'))
+ ({ NanoPow } = await import('https://zoso.dev/?p=nano-pow.git;a=blob_plain;f=main.min.js;hb=refs/heads/main'))
} catch (err) {
console.warn(err)
try {
- ({ NanoPow, NanoPowGl, NanoPowGpu } = await import('https://cdn.jsdelivr.net/npm/nano-pow@latest/dist/main.min.js'))
+ ({ NanoPow } = await import('https://cdn.jsdelivr.net/npm/nano-pow@latest/dist/main.min.js'))
} catch (err) {
throw new Error(`Failed to load NanoPow ${err}`)
}
}
}
- function random (size = 32) {
- const bytes = new Uint8Array(size)
- crypto.getRandomValues(bytes)
+ const glSize = (canvas => {
+ const gl = canvas.getContext('webgl2')
+ canvas.height = gl.getParameter(gl.MAX_VIEWPORT_DIMS)[0]
+ canvas.width = gl.getParameter(gl.MAX_VIEWPORT_DIMS)[1]
+ return Math.min(gl.drawingBufferHeight, gl.drawingBufferWidth)
+ })(new OffscreenCanvas(0, 0))
+
+ function random (size = 64) {
let hex = ''
- for (let i = 0; i < size; i++) hex += bytes[i].toString(16).padStart(2, '0')
- return hex
+ while (hex.length < size) {
+ hex += crypto.randomUUID().replace(/-.*-/g, '')
+ }
+ return hex.slice(0, size)
}
function average (times, type, effort) {
- let count = times.length, truncatedCount = 0, sum = 0, truncated = 0, reciprocals = 0, logarithms = 0, min = Number.MAX_SAFE_INTEGER, max = 0, median = 0, rate = 0
- times.sort((a, b) => a - b)
- for (let i = 0; i < count; i++) {
- sum += times[i]
- reciprocals += 1 / times[i]
- logarithms += Math.log(times[i])
- min = Math.min(min, times[i])
- max = Math.max(max, times[i])
- if (i + 1 === Math.floor(count / 2)) {
- median = times[i]
- }
- if (count < 3 || (i > (0.1 * count) && i < (0.9 * (count - 1)))) {
- truncated += times[i]
- truncatedCount++
- }
- }
+ const averages = stats(times)
const title = type === 'WebGPU'
? `NanoPow (${type}) | Effort: ${effort} | Dispatch: ${(0x100 * effort) ** 2} | Threads: ${8 * 8 * (0x100 * effort) ** 2}`
- : `NanoPow (${type}) | Effort: ${effort}`
+ : type === 'WebGL'
+ ? `NanoPow (${type}) | Effort: ${effort} | Work per frame: ${Math.min(0x100 * effort, glSize) ** 2}`
+ : `NanoPow (${type}) | Effort: ${effort}`
return {
- [title]: {
- count: count,
- total: sum,
- rate: 1000 * truncatedCount / (truncated || sum),
- min: min,
- max: max,
- median: median,
- arithmetic: sum / count,
- truncated: truncated / truncatedCount,
- harmonic: count / reciprocals,
- geometric: Math.exp(logarithms / count)
- }
+ [title]: averages
}
}
- export async function run (difficulty, size, effort, isOutputShown, isGlForced, isDebug) {
- const NP = isGlForced ? NanoPowGl : NanoPow
- const type = (NP === NanoPowGpu) ? 'WebGPU' : (NP === NanoPowGl) ? 'WebGL' : 'unknown API'
- console.log(`%cNanoPow`, 'color:green', 'Checking validation against known values')
-
- const expect = []
- let result
-
- // PASS
- result = await NP.work_validate('47c83266398728cf', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', { debug: isDebug })
- result = result.valid_all === '1'
- console.log(`work_validate() output for good nonce 1 is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result)
-
- result = await NP.work_validate('4a8fb104eebbd336', '8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01', { debug: isDebug })
- result = result.valid_all === '1'
- console.log(`work_validate() output for good nonce 2 is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result)
-
- result = await NP.work_validate('c5d5d6f7c5d6ccd1', '281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117', { debug: isDebug })
- result = result.valid_all === '1'
- console.log(`work_validate() output for colliding nonce is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result)
-
- result = await NP.work_validate('6866c1ac3831a891', '7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090', { difficulty: 0xfffffe0000000000n, debug: isDebug })
- result = result.valid === '1' && result.valid_all === '0' && result.valid_receive === '1'
- console.log(`work_validate() output for good receive difficulty nonce is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result)
-
- // XFAIL
- result = await NP.work_validate('0000000000000000', '0000000000000000000000000000000000000000000000000000000000000000', { debug: isDebug })
- result = result.valid_all === '0'
- console.log(`work_validate() output for bad nonce 1 is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result)
-
- result = await NP.work_validate('c5d5d6f7c5d6ccd1', 'BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E', { debug: isDebug })
- result = result.valid_all === '0'
- console.log(`work_validate() output for bad nonce 2 is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result)
+ export async function run (difficulty, size, effort, isOutputShown, api, isDebug, isSelfCheck) {
+ const type = api
+ api = type.toLowerCase()
+ if (isSelfCheck) {
+ document.getElementById('status').innerHTML = `RUNNING SELF-CHECK`
+ console.log(`%cNanoPow`, 'color:green', 'Checking validation against known values')
- try {
- result = await NP.work_validate('ae238556213c3624', 'BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6', { difficulty: 0xfffffff800000001n, debug: isDebug })
- console.log('boo')
- } catch (err) {
- result = null
- }
- result = result === null
- console.log(`work_validate() output for bad difficulty beyond max is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result)
+ const expect = []
+ let result
- result = await NP.work_validate('29a9ae0236990e2e', '32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F', { debug: isDebug })
- result = result.valid_all === '0'
- console.log(`work_validate() output for slightly wrong nonce is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result)
+ // PASS
+ result = await NanoPow.work_validate('47c83266398728cf', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', { api, debug: isDebug })
+ result = result.valid_all === '1'
+ console.log(`work_validate() output for good nonce 1 is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
- result = await NP.work_validate('7d903b18d03f9820', '39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150', { difficulty: 0xfffffe0000000000n, debug: isDebug })
- result = result.valid === '0' && result.valid_all === '0' && result.valid_receive === '0'
- console.log(`work_validate() output for bad receive difficulty nonce is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result)
+ result = await NanoPow.work_validate('4a8fb104eebbd336', '8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01', { api, debug: isDebug })
+ result = result.valid_all === '1'
+ console.log(`work_validate() output for good nonce 2 is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
- try {
- if (!expect.every(result => result)) throw new Error(`Validation is not working`)
- } catch (err) {
- document.getElementById('status').innerHTML = `FAILED TO VALIDATE KNOWN VALUES`
- document.getElementById('output').innerHTML += `Error: ${err.message}<br/>`
- console.error(err)
- return
+ result = await NanoPow.work_validate('c5d5d6f7c5d6ccd1', '281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117', { api, debug: isDebug })
+ result = result.valid_all === '1'
+ console.log(`work_validate() output for colliding nonce is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
+
+ result = await NanoPow.work_validate('6866c1ac3831a891', '7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090', { api, difficulty: 0xfffffe0000000000n, debug: isDebug })
+ result = result.valid === '1' && result.valid_all === '0' && result.valid_receive === '1'
+ console.log(`work_validate() output for good receive difficulty nonce is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
+
+ // XFAIL
+ result = await NanoPow.work_validate('0000000000000000', '0000000000000000000000000000000000000000000000000000000000000000', { api, debug: isDebug })
+ result = result.valid_all === '0'
+ console.log(`work_validate() output for bad nonce 1 is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
+
+ result = await NanoPow.work_validate('c5d5d6f7c5d6ccd1', 'BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E', { api, debug: isDebug })
+ result = result.valid_all === '0'
+ console.log(`work_validate() output for bad nonce 2 is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
+
+ result = await NanoPow.work_validate('ae238556213c3624', 'BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6', { api, difficulty: 0xfffffff800000001n, debug: isDebug })
+ result = result.error === 'Invalid difficulty fffffff800000001'
+ console.log(`work_validate() output for bad difficulty beyond max is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
+
+ result = await NanoPow.work_validate('29a9ae0236990e2e', '32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F', { api, debug: isDebug })
+ result = result.valid_all === '0'
+ console.log(`work_validate() output for slightly wrong nonce is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
+
+ result = await NanoPow.work_validate('7d903b18d03f9820', '39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150', { api, difficulty: 0xfffffe0000000000n, debug: isDebug })
+ result = result.valid === '0' && result.valid_all === '0' && result.valid_receive === '0'
+ console.log(`work_validate() output for bad receive difficulty nonce is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
+
+ const prefixes = [
+ '0B1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111',
+ '0b1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111',
+ '0O17777777777777777777777777777777777777777777777777777777777777777777777777777777777777',
+ '0o17777777777777777777777777777777777777777777777777777777777777777777777777777777777777',
+ '0Xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
+ '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
+ ' 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn ',
+ ' ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn ',
+ ]
+ for (let i = 0; i < prefixes.length; i++) {
+ const hash = prefixes[i]
+ let result = null
+ const start = performance.now()
+ try {
+ result = await NanoPow.work_generate(hash, { api, difficulty, effort, debug: isDebug })
+ } catch (err) {
+ document.getElementById('output').innerHTML += `Error: ${err.message}<br/>`
+ console.error(err)
+ return
+ }
+ const end = performance.now()
+ const check = await NanoPow.work_validate(result.work, result.hash, { difficulty, debug: isDebug })
+ const isValid = (check.valid === '1' && BigInt(`0x${result.hash}`) === BigInt(hash.replace('n', '').replace(' f', '0xf')))
+ console.log(`work_generate() output for max value block hash is ${isValid === true ? 'correct' : 'incorrect'}`)
+ expect.push(isValid)
+ }
+
+ try {
+ if (!expect.every(result => result)) throw new Error(`Validation is not working`)
+ } catch (err) {
+ document.getElementById('status').innerHTML = `FAILED TO VALIDATE KNOWN VALUES`
+ document.getElementById('output').innerHTML += `Error: ${err.message}<br/>`
+ console.error(err)
+ return
+ }
}
document.getElementById('status').innerHTML = `TESTING IN PROGRESS 0/${size}`
let result = null
const start = performance.now()
try {
- result = await NP.work_generate(hash, { difficulty, effort, debug: isDebug })
+ result = await NanoPow.work_generate(hash, { api, difficulty, effort, debug: isDebug })
} catch (err) {
document.getElementById('output').innerHTML += `Error: ${err.message}<br/>`
console.error(err)
return
}
const end = performance.now()
- const check = await NP.work_validate(result.work, result.hash, { difficulty, debug: isDebug })
+ const check = await NanoPow.work_validate(result.work, result.hash, { difficulty, debug: isDebug })
const isValid = (result.hash === hash && check.valid === '1') ? 'VALID' : 'INVALID'
times.push(end - start)
const msg = `${isValid} (${end - start} ms)\n${JSON.stringify(result, ' ', 2)}`
}
function startValidation (event) {
- const difficulty = document.getElementById('difficulty')
- const isGlForced = document.getElementById('isGlForced')
- const work = document.getElementById('work')
- const hash = document.getElementById('hash')
+ const difficulty = document.getElementById('difficulty')?.value
+ const work = document.getElementById('work')?.value
+ const hash = document.getElementById('hash')?.value
const validation = document.getElementById('validation')
+ const api = document.getElementById('api')?.value
+ const apiContainer = document.getElementById('api')?.parentElement
+ if (api === 'CPU') {
+ apiContainer.classList.add('warning')
+ apiContainer.title = 'Use only for testing with very low difficulty.'
+ } else {
+ apiContainer.classList.remove('warning')
+ apiContainer.title = ''
+ }
validation.innerText = '⏳'
- if (work.value.length === 16 && hash.value.length === 64) {
- const NP = isGlForced ? NanoPowGl : NanoPow
- NP.work_validate(work.value, hash.value, { difficulty: difficulty.value })
+ if (work.length === 16 && hash.length === 64) {
+ NanoPow.work_validate(work, hash, { difficulty })
.then(result => {
validation.innerText = result
? '✔️'
document.getElementById('difficulty').addEventListener('input', startValidation)
document.getElementById('work').addEventListener('input', startValidation)
document.getElementById('hash').addEventListener('input', startValidation)
+ document.getElementById('api').addEventListener('input', startValidation)
function startTest (event) {
+ event.target.disabled = true
const difficulty = document.getElementById('difficulty')
const size = document.getElementById('size')
const effort = document.getElementById('effort')
const isOutputShown = document.getElementById('isOutputShown')
- const isGlForced = document.getElementById('isGlForced')
+ const api = document.getElementById('api')
const isDebug = document.getElementById('isDebug')
- run(difficulty.value, +size.value, +effort.value, isOutputShown.checked, isGlForced.checked, isDebug.checked)
+ const isSelfCheck = document.getElementById('isSelfCheck')
+ run(difficulty.value, +size.value, +effort.value, isOutputShown.checked, api.value, isDebug.checked, isSelfCheck.checked)
+ .then(() => {
+ event.target.disabled = false
+ isSelfCheck.checked = false
+ })
}
document.getElementById('btnStartTest').addEventListener('click', startTest)
document.getElementById('effort').value = Math.max(1, Math.floor(navigator.hardwareConcurrency) / 2)
body{background:black;color:white;}a{color:darkcyan;}input[type=number]{width:5em;}span{margin:0.5em;}
label.hex::after{color:grey;content:'0x';display:inline-block;font-size:90%;left:0.5em;position:relative;width:0;}
label.hex+input{padding-left:1.5em;}
+ .warning::after{content:'⚠️'}
</style>
</head>
<label for="effort">Effort (1-32)</label>
<input id="effort" type="number" min="1" max="32" />
<span>
- <label for="isOutputShown">Show output?</label>
- <input id="isOutputShown" type="checkbox" checked />
+ <label for="api">API</label>
+ <select id="api">
+ <option>WebGPU</option>
+ <option>WebGL</option>
+ <option>WASM</option>
+ <option>CPU</option>
+ </select>
</span>
<span>
- <label for="isGlForced">Force WebGL?</label>
- <input id="isGlForced" type="checkbox" />
+ <label for="isOutputShown">Show output?</label>
+ <input id="isOutputShown" type="checkbox" checked />
</span>
<span>
<label for="isDebug">Debug?</label>
<input id="isDebug" type="checkbox" />
</span>
+ <span>
+ <label for="isSelfCheck">Run self-check?</label>
+ <input id="isSelfCheck" type="checkbox" checked />
+ </span>
<button id="btnStartTest">Go</button>
<h3 id="status">WAITING</h3>
<hr />
export NANO_POW_EFFORT=4
export NANO_POW_PORT=3001
+printf '\nTest CLI benchmark\n'
+"$SCRIPT_DIR"/../dist/bin/nano-pow.sh --benchmark 10 --effort 4 --debug
+
+printf '\nTest CLI piped input\n'
+cat "$SCRIPT_DIR"/blockhashes.txt | "$SCRIPT_DIR"/../dist/bin/nano-pow.sh --effort 4 --debug
+
+printf '\nLaunching test server\n'
"$SCRIPT_DIR"/../dist/bin/nano-pow.sh --server
sleep 3s
curl -d '{ "action": "work_validate", "work": "29a9ae0236990e2e", "hash": "32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F" }' localhost:3001
curl -d '{ "action": "work_validate", "work": "ae238556213c3624", "hash": "BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6", "difficulty": "fffffff700000000" }' localhost:3001
curl -d '{ "action": "work_validate", "work": "7d903b18d03f9820", "hash": "39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150", "difficulty": "fffffe0000000000" }' localhost:3001
-curl -d '{ "action": "work_validate", "work": "e45835c3b291c3d1", "hash": "9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F", "difficulty": "fffffff700000000" }' localhost:3001
-printf '\nGenerate\n'
+printf '\nTest generate\n'
curl -d '{ "action": "work_generate", "hash": "0D653EBFE692CA655449B301DBCEEF4E4A47454E3DFDF8C87B87C50DB6530A25" }' localhost:3001
curl -d '{ "action": "work_generate", "hash": "E93104CFC1CE40B2A29F0E85807E7E51E521C30E2B79A8EDDDB2D771DCF5C06B" }' localhost:3001
curl -d '{ "action": "work_generate", "hash": "472F8F5D908AF51E9583950D50BAE6A8C5FD66D6984FA62D4D31ADDEA5AA3AC7" }' localhost:3001
"strict": true,
"rootDir": "src",
"paths": {
+ "#lib/*": [
+ "./src/lib/*"
+ ],
"#types": [
"./src/types.d.ts"
],
"include": [
"src/main.ts",
"src/**/*.ts"
+ ],
+ "exclude": [
+ "src/lib/generate/wasm/asm"
]
}