Add two new methods which align with Nano node RPC actions. Return more data from hash result. Indicate a more specific `threshold` by passing a full 16-character string to the new `work_` methods. Changed `NanoPowGl.size` property to return total pixel count. Add typings for request and response objects consumed by new `work_` methods. Add more tests for receive threshold edge cases. Improve build process. Expand documentation.
build/\r
dist/\r
types/\r
+\r
+# IDE\r
+.vs/\r
+.vscode/\r
Fix minor issues with test page.
Add benchmark results table.
+
+
+
+## v2.0.0
+
+### Notable Changes
+
+#### Control behavior with configuration options
+
+While NanoPow currently supports configuration of a custom minimum threshold in
+order to support send, receive, and legacy level of difficulty, additional
+options allow developers to control behavior according to their needs.
+
+Instead of a threshold parameter, `search` and `validate` accept a new options
+object parameter.
+
+```javascript
+NanoPow.search(blockhash, { threshold: 'fffffc00', effort: 16, debug: true })
+```
+
+**This is a breaking change.** Integrations already using the optional threshold
+parameter should update calls to pass { threshold: <value> } instead.
+
+#### Align with reference implementation of BLAKE2b
+
+Nano uses the BLAKE2b hashing algorithm for calculating proof-of-work and other
+operations. The reference implementation of BLAKE2b uses 64-bit unsigned integer
+operands, but WebGL and WebGPU only support 32-bit uints. Previous designs used
+pairs of `u32` variables to represent high and low bits of a single `u64`.
+
+Now, the same approach is used but implemented with vectors instead of scalars,
+i.e. `vec2<u32>` instead of `u32`. This has several benefits:
+* Integer pairs of high and low bits are held in memory together, improving
+legibility as well as alignment with the original C reference implementation.
+* Arithmetic and bitwise operations are executed on both high and low bits in
+parallel using one statement instead of three.
+* File size is reduced by over 58%.
+
+### Other Changes
+
+Expand the options available on the sample testing page.
+
+Fix a bug with the validate function where a valid but different nonce would be
+reported as a match to the input nonce.
+
+Fix a bug with the truncated average benchmark calculation.
+
+
+
+## v1.2.4
+
+### Notable Changes
+
+Fix duplicate code on testing page due to bad merge commit.
+
+Increase WebGPU dispatch size.
+
+
+
+## v1.2.3
+
+### Notable Changes
+
+Add bespoke TypeScript definition file.
+
+Remove usage of package `imports` property and use regular relative paths.
+
+
+
+## v1.2.2
+
+### Notable Changes
+
+Compile to `dist/` instead of `build/`.
+
+
+
+## v1.2.1
+
+### Notable Changes
+
+Rename bundle output file.
+
+
+
+## v1.2.0
+
+### Notable Changes
+
+#### Improved fallback behavior
+
+Integrating a tool like NanoPow should be as painless as possible, even when
+using it in unsupported environments.
+
+Now, browser support for WebGPU and WebGL is checked and the faster API set to
+the general `NanoPow` module, so there is no need anymore to import both
+implementations and choose manually, although that is still an option if
+desired.
+
+### Other Changes
+
+Clarify the README with better instructions.
+
+Provide additional context about test progress on the testing webpage.
+
+
+
+## v1.1.0
+
+### Notable Changes
+
+#### Validate existing work
+
+Test a previously calculated PoW value against a block hash to check if it meets
+Nano work threshold requirements.
+
+#### Other Changes
+
+Fix packaging bugs to eliminate npm errors about imports and types.
+
+Fix repo cloning so that `git clone https://zoso.dev/nano-pow.git` works as
+expected (thanks to u/the_azarian for helping track this down).
+
+
+
+## v1.0.0
+
+### Notable Changes
+
+#### NanoPow released!
+
+Written in Javascript and WGSL, NanoPow leverages the cutting edge WebGPU API to
+achieve massively increased proof-of-work speed for supported devices directly
+in the browser!
+
+* Faster than any other JavaScript implementation of Nano proof-of-work.
+* Avoids graphical stuttering seen in WebGL implementations.
+* Works entirely offline and locally on the device.
+* Zero external dependencies. Run it in any environment that supports WebGPU
+compute shaders.
+* Released under the GPLv3 license to promote user freedom and FOSS principles.
+
+Full announcement: https://www.reddit.com/r/nanocurrency/comments/1hz841f/announcing_nanopow_local_proofofwork_at/
bundle: true,
platform: 'browser',
entryPoints: [
- { out: 'main.min', in: './src/main.js' }
+ { in: './src/main.ts', out: 'main.min' },
+ { in: './src/types.d.ts', out: 'types.d' }
],
+ loader: {
+ '.d.ts': 'copy'
+ },
format: 'esm',
legalComments: 'inline',
outdir: 'dist',
target: 'esnext',
plugins: [
glsl({
- minify: true
+ minify: true,
+ preserveLegalComments: true
})
]
})
+
+await build({
+ bundle: false,
+ platform: 'node',
+ entryPoints: [
+ './src/bin/*.ts'
+ ],
+ format: 'esm',
+ legalComments: 'inline',
+ outdir: 'dist/bin',
+ target: 'esnext'
+})
},
"devDependencies": {
"@types/node": "^22.13.10",
- "@webgpu/types": "^0.1.55",
+ "@webgpu/types": "^0.1.57",
"esbuild": "^0.25.1",
- "esbuild-plugin-glsl": "^1.2.3",
+ "esbuild-plugin-glsl": "^1.4.0",
"typescript": "^5.8.2"
},
"funding": {
}
},
"node_modules/@webgpu/types": {
- "version": "0.1.55",
- "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.55.tgz",
- "integrity": "sha512-p97I8XEC1h04esklFqyIH+UhFrUcj8/1/vBWgc6lAK4jMJc+KbhUy8D4dquHYztFj6pHLqGcp/P1xvBBF4r3DA==",
+ "version": "0.1.57",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.57.tgz",
+ "integrity": "sha512-w8IuWOmgeb9bA9swqSyG6Z/KdKfCdGPxZ75YJUpsJBF4Q/RT5rnPuStzuxOXihcrSjGO4/maH+M9PQgbBVs69A==",
"dev": true,
"license": "BSD-3-Clause"
},
}
},
"node_modules/bare-os": {
- "version": "3.5.1",
- "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.5.1.tgz",
- "integrity": "sha512-LvfVNDcWLw2AnIw5f2mWUgumW3I3N/WYGiWeimhQC1Ybt71n2FjlS9GJKeCnFeg1MKZHxzIFmpFnBXDI+sBeFg==",
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.0.tgz",
+ "integrity": "sha512-BUrFS5TqSBdA0LwHop4OjPJwisqxGy6JsWVqV6qaFoe965qqtaKfDzHY5T2YA1gUL0ZeeQeA+4BBc1FJTcHiPw==",
"license": "Apache-2.0",
"optional": true,
"engines": {
}
},
"node_modules/esbuild-plugin-glsl": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/esbuild-plugin-glsl/-/esbuild-plugin-glsl-1.2.3.tgz",
- "integrity": "sha512-PUM4rGm0ZBZI46Q9sF7XNZqEhVX1aa8Pxnh+kWPDMyeY7CPO4oEeC4wmKeTBmQekMuiRDB/tdAr0K79ZUbnmtQ==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/esbuild-plugin-glsl/-/esbuild-plugin-glsl-1.4.0.tgz",
+ "integrity": "sha512-8rWi34P9CKhNyzS3VSMw0F58NlIXf7u7n08c1Xtgce4dMR9uWgkuHXdZxTKsY/4YeLwVrGc22tA6SG4600v8Yw==",
"dev": true,
"license": "Zlib",
"engines": {
- "node": ">=18"
+ "node": ">= 18"
},
"peerDependencies": {
"esbuild": "0.x.x"
"url": "git+https://zoso.dev/nano-pow.git"
},
"scripts": {
- "build": "rm -rf {dist,types} && tsc && node esbuild.mjs && npm run fix-copyright && cp -r src/bin src/types.d.ts dist",
- "fix-copyright": "sed -i '/\\/\\/ src\\/shaders\\/compute\\.wgsl/a //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\\n//! SPDX-License-Identifier: GPL-3.0-or-later' dist/main.min.js"
+ "build": "rm -rf {dist,types} && tsc && node esbuild.mjs"
},
"devDependencies": {
"@types/node": "^22.13.10",
- "@webgpu/types": "^0.1.55",
+ "@webgpu/types": "^0.1.57",
"esbuild": "^0.25.1",
- "esbuild-plugin-glsl": "^1.2.3",
+ "esbuild-plugin-glsl": "^1.4.0",
"typescript": "^5.8.2"
},
"type": "module",
import * as readline from 'node:readline/promises'
import * as puppeteer from 'puppeteer'
-const hashes = []
+const hashes: string[] = []
-const stdinErrors = []
+const stdinErrors: string[] = []
if (!process.stdin.isTTY) {
const stdin = readline.createInterface({
input: process.stdin
process.exit()
}
-const inArgs = []
+const inArgs: string[] = []
while (/^[0-9A-Fa-f]{64}$/.test(args[args.length - 1] ?? '')) {
- inArgs.unshift(args.pop())
+ inArgs.unshift(args.pop() as string)
}
hashes.push(...inArgs)
const browser = await puppeteer.launch({
headless: true,
args: [
- '--no-sandbox',
'--headless=new',
'--use-angle=vulkan',
'--enable-features=Vulkan',
'--disable-vulkan-surface',
- '--enable-unsafe-webgpu',
- '--enable-vulkan'
+ '--enable-unsafe-webgpu'
]
})
const page = await browser.newPage()
if (output[1] === 'exit') {
if (isJson) {
const results = await page.evaluate(() => {
- return window.results
+ return (window as any).results
})
for (let i = 0; i < results.length; i++) {
results[i] = {
+#version 300 es
+#pragma vscode_glsllint_stage: frag
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
//! SPDX-License-Identifier: GPL-3.0-or-later
-export const NanoPowGlDownsampleShader = `#version 300 es
-#pragma vscode_glsllint_stage: frag
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
pixel = texture(src, blockCoord + vec2(texel.x, texel.y));
nonce = pixel.x == 0u ? nonce : pixel;
}
-`
+#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
-export const NanoPowGlDrawShader = `#version 300 es
-#pragma vscode_glsllint_stage: frag
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
// blockhash - Array of precalculated block hash components
// threshold - 0xfffffff8 for send/change blocks, 0xfffffe00 for all else
-// search - Checks all pixels if true, else only checks 1 pixel to validate
+// validate - If true, checks only 1 pixel to validate, else checks all pixels to search
layout(std140) uniform UBO {
uint blockhash[8];
- uint threshold;
- bool search;
+ uvec2 threshold;
+ bool validate;
};
// Random work seed values
G(2u, 7u, 8u, 13u, m[11u], m[7u], 3u, 4u, 9u, 14u, m[5u], m[3u]);
// Pixel data set from work seed values
- // Finalize digest from high bits, low bits can be safely ignored
- if ((BLAKE2B_INIT[0u].y ^ v[0u].y ^ v[8u].y) >= threshold && (search || uvec2(gl_FragCoord) == uvec2(0u))) {
- nonce = uvec4(1u, m[0u].y, m[0u].x, (uint(gl_FragCoord.x) << 16u) | uint(gl_FragCoord.y));
+ uvec2 result = BLAKE2B_INIT[0u] ^ v[0u] ^ v[8u];
+ if ((validate && uvec2(gl_FragCoord) == uvec2(0u)) || (result.y > threshold.y || (result.y == threshold.y && result.x > threshold.x))) {
+ nonce = uvec4((BLAKE2B_INIT[0u] ^ v[0u] ^ v[8u]), m[0u]).yxwz;
}
// Valid nonce not found
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
-
-export const NanoPowGlVertexShader = `#version 300 es
-#pragma vscode_glsllint_stage: vert
-#ifdef GL_FRAGMENT_PRECISION_HIGH
-precision highp float;
-#else
-precision mediump float;
-#endif
-
-layout (location=0) in vec4 position;
-
-void main() {
- gl_Position = position;
-}
-`
--- /dev/null
+#version 300 es
+#pragma vscode_glsllint_stage: vert
+//! 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
+
+layout (location=0) in vec4 position;
+
+void main() {
+ gl_Position = position;
+}
//! SPDX-FileContributor: Ben Green <ben@latenightsketches.com>
//! SPDX-License-Identifier: GPL-3.0-or-later AND MIT
-import { NanoPowGlDownsampleShader } from './gl-downsample.js'
-import { NanoPowGlDrawShader } from './gl-draw.js'
-import { NanoPowGlVertexShader } from './gl-vertex.js'
-import type { FBO, NanoPowOptions } from '../../types.d.ts'
-
+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 type { FBO, NanoPowOptions, WorkGenerateRequest, WorkGenerateResponse, WorkValidateRequest, WorkValidateResponse } from '../../types.d.ts'
+
+/**
+* Nano proof-of-work using WebGL 2.0.
+*/
export class NanoPowGl {
+ static #SEND: bigint = 0xfffffff800000000n
+ static #RECEIVE: bigint = 0xfffffe0000000000n
+
static #busy: boolean = false
static #debug: boolean = false
static #raf: number = 0
static #cores: number = Math.max(1, Math.floor(navigator.hardwareConcurrency))
static #WORKLOAD: number = 256 * this.#cores
static #canvas: OffscreenCanvas
- static get size () { return this.#gl?.drawingBufferWidth }
+ /** Drawing buffer size in pixels. */
+ static get size (): number { return (this.#gl?.drawingBufferWidth ?? 0) * (this.#gl?.drawingBufferHeight ?? 0) }
static #gl: WebGL2RenderingContext | null
static #drawProgram: WebGLProgram | null
static #query: WebGLQuery | null
static #pixels: Uint32Array
- /**Vertex Positions, 2 triangles */
+ /** Vertex positions for fullscreen quad. */
static #positions: Float32Array = new Float32Array([
-1, -1, 1, -1, 1, 1, -1, 1
])
- /** Compile */
+ /**
+ * Constructs canvas, gets WebGL context, initializes buffers, and compiles
+ * shaders.
+ */
static async init (): Promise<void> {
if (this.#busy) return
this.#busy = true
/** Finalize configuration */
this.#query = this.#gl.createQuery()
- this.#pixels = new Uint32Array(this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight * 4)
- console.log(`NanoPow WebGL initialized at ${this.#gl.drawingBufferWidth}x${this.#gl.drawingBufferHeight}. Maximum nonces checked per frame: ${this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight}`)
+ this.#pixels = new Uint32Array(this.size * 4)
+ console.log(`NanoPow WebGL initialized at ${this.#gl.drawingBufferWidth}x${this.#gl.drawingBufferHeight}. Maximum nonces checked per frame: ${this.size}`)
} catch (err) {
throw new Error('WebGL initialization failed.', { cause: err })
} finally {
}
}
+ /**
+ * 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)
* @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): 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')
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].toString(16).padStart(8, '0')})`)
+ 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 hex = `${this.#pixels[i + 1].toString(16).padStart(8, '0')}${this.#pixels[i + 2].toString(16).padStart(8, '0')}`
- if (workHex == null || workHex == hex) return hex
+ 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
+ * @param {NanoPowOptions} options - Used to configure search execution
*/
static async search (hash: string, options?: NanoPowOptions): Promise<string> {
+ if (options?.threshold != null) {
+ options.threshold = BigInt(`0x${options.threshold.toString(16) ?? '0'}00000000`)
+ }
+ const result = await this.work_generate(hash, options)
+ return result.work
+ }
+
+ /**
+ * 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 (this.#busy) {
console.log('NanoPowGl is busy. Retrying search...')
return new Promise(resolve => {
setTimeout(async (): Promise<void> => {
- const result = this.search(hash, options)
+ const result = this.work_generate(hash, options)
resolve(result)
}, 100)
})
/** Process user input */
if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`)
- const threshold = (typeof options?.threshold !== 'number' || options.threshold < 0x0 || options.threshold > 0xffffffff)
- ? 0xfffffff8
+ const threshold = (typeof options?.threshold !== 'bigint' || options.threshold < 1n || options.threshold > 0xffffffffffffffffn)
+ ? 0xfffffff800000000n
: options.threshold
const effort = (typeof options?.effort !== 'number' || options.effort < 0x1 || options.effort > 0x20)
? this.#cores
this.#debug = !!(options?.debug)
if (this.#debug) console.log('NanoPowGl.search()')
if (this.#debug) console.log('blockhash', hash)
- if (this.#debug) console.log('search options', JSON.stringify(options))
+ 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) {
const uint32 = hash.slice(i, i + 8)
this.#uboView.setUint32(i * 2, parseInt(uint32, 16))
}
- this.#uboView.setUint32(128, threshold, true)
- this.#uboView.setUint32(132, 1, true)
+ this.#uboView.setBigUint64(128, threshold, 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)
/** Start drawing to calculate one nonce per pixel */
let times = []
let start = performance.now()
- let nonce = null
+ let result = 0n
+ let nonce = 0n
+ let found = false
+
if (this.#debug) console.groupCollapsed('Seeds (click to view)')
- while (nonce == null) {
+ 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)
- const found = await this.#checkQueryResult()
+ found = await this.#checkQueryResult()
times.push(performance.now() - start)
if (found) {
+ ({ result, nonce } = this.#readResult())
if (this.#debug) console.groupEnd()
- nonce = this.#readResult()
}
}
this.#busy = false
if (this.#debug) this.#logAverages(times)
- return nonce
+ return {
+ hash,
+ work: nonce.toString(16).padStart(16, '0'),
+ difficulty: result.toString(16).padStart(16, '0')
+ }
}
/**
* @param {NanoPowOptions} options - Options used to configure search execution
*/
static async validate (work: string, hash: string, options?: NanoPowOptions): Promise<boolean> {
+ if (options?.threshold != null) {
+ options.threshold = BigInt(`0x${options.threshold.toString(16) ?? '0'}00000000`)
+ }
+ const result = await this.work_validate(work, hash, options)
+ return (options?.threshold != null)
+ ? result.valid === '1'
+ : result.valid_all === '1'
+ }
+
+ /**
+ * 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 (this.#busy) {
console.log('NanoPowGl is busy. Retrying validate...')
return new Promise(resolve => {
setTimeout(async (): Promise<void> => {
- const result = this.validate(work, hash, options)
+ const result = this.work_validate(work, hash, options)
resolve(result)
}, 100)
})
/** Process user input */
if (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new Error(`Invalid work ${work}`)
if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`)
- const threshold = (typeof options?.threshold !== 'number' || options.threshold < 0x0 || options.threshold > 0xffffffff)
- ? 0xfffffff8
+ const threshold = (typeof options?.threshold !== 'bigint' || options.threshold < 1n || options.threshold > 0xffffffffffffffffn)
+ ? 0xfffffff800000000n
: options.threshold
this.#debug = !!(options?.debug)
if (this.#debug) console.log('NanoPowGl.validate()')
if (this.#debug) console.log('blockhash', hash)
- if (this.#debug) console.log('validate options', JSON.stringify(options))
+ 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')
const uint32 = hash.slice(i, i + 8)
this.#uboView.setUint32(i * 2, parseInt(uint32, 16))
}
- this.#uboView.setUint32(128, threshold, true)
- this.#uboView.setUint32(132, 0, true)
+ this.#uboView.setBigUint64(128, threshold, 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 nonce = null
+ 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 {
- nonce = this.#readResult(work)
+ ({ result, nonce } = this.#readResult(work))
} catch (err) {
- found = false
+ throw new Error('Error reading results', { cause: err })
}
+ } else {
+ throw new Error('Failed to find nonce on canvas')
}
this.#busy = false
- if (found && nonce !== work) throw new Error(`Nonce found but does not match work`)
- return found
+ 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?.threshold != null) response.valid = (result >= threshold) ? '1' : '0'
+ return response
}
}
struct UBO {
blockhash: array<vec4<u32>, 2>,
seed: vec2<u32>,
- threshold: u32
+ threshold: vec2<u32>
};
@group(0) @binding(0) var<uniform> ubo: UBO;
found = (local_id.x == 0u && atomicLoad(&work.found) != 0u);
workgroupBarrier();
if (found) { return; }
- main(global_id);
+ main(global_id, false);
}
/**
*/
@compute @workgroup_size(1)
fn validate(@builtin(global_invocation_id) global_id: vec3<u32>) {
- main(global_id);
+ main(global_id, true);
}
/**
* implementation assigns each array element to its own variable to enhance
* performance, but the variable name still contains the original index digit.
*/
-fn main(id: vec3<u32>) {
+fn main(id: vec3<u32>, validate: bool) {
/**
* Initialize (nonce||blockhash) concatenation
*/
* Set nonce if it passes the threshold and no other thread has set it.
* Only high bits are needed for comparison since threshold low bits are zero.
*/
- if ((BLAKE2B_INIT[0u].y ^ v01.y ^ v89.y) >= ubo.threshold && atomicLoad(&work.found) == 0u) {
+ var result = BLAKE2B_INIT[0u] ^ v01.xy ^ v89.xy;
+ if (validate || ((result.y > ubo.threshold.y || (result.y == ubo.threshold.y && result.y >= ubo.threshold.y)) && atomicLoad(&work.found) == 0u)) {
atomicStore(&work.found, 1u);
work.nonce = m0;
- work.result = (BLAKE2B_INIT[0u] ^ v01.xy ^ v89.xy);
+ work.result = result;
}
return;
}
/// <reference types="@webgpu/types" />
import { default as NanoPowGpuComputeShader } from './compute.wgsl'
-import type { NanoPowOptions } from '../../types.d.ts'
+import type { NanoPowOptions, WorkGenerateRequest, WorkGenerateResponse, WorkValidateRequest, WorkValidateResponse } from '../../types.d.ts'
+
/**
* Nano proof-of-work using WebGPU.
*/
export class NanoPowGpu {
+ static #SEND: bigint = 0xfffffff800000000n
+ static #RECEIVE: bigint = 0xfffffe0000000000n
// Initialize WebGPU
static #busy: boolean = false
console.table(averages)
}
- static async #dispatch (pipeline: GPUComputePipeline, seed: bigint, hash: string, threshold: number, passes: number): Promise<DataView> {
+ static async #dispatch (pipeline: GPUComputePipeline, seed: bigint, hash: string, threshold: bigint, passes: number): Promise<DataView> {
if (this.#device == null) throw new Error(`WebGPU device failed to load.`)
// Set up uniform buffer object
// Note: u32 size is 4, but total alignment must be multiple of 16
this.#uboView.setBigUint64(i / 2, BigInt(`0x${u64}`))
}
this.#uboView.setBigUint64(32, seed, true)
- this.#uboView.setUint32(40, threshold, true)
+ this.#uboView.setBigUint64(40, threshold, true)
if (this.#debug) console.log('UBO', this.#uboView)
this.#device.queue.writeBuffer(this.#uboBuffer, 0, this.#uboView)
* @param {NanoPowOptions} options - Used to configure search execution
*/
static async search (hash: string, options?: NanoPowOptions): Promise<string> {
+ if (options?.threshold != null) {
+ options.threshold = BigInt(`0x${options.threshold.toString(16) ?? '0'}00000000`)
+ }
+ const result = await this.work_generate(hash, options)
+ return result.work
+ }
+
+ /**
+ * 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> {
if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new TypeError(`Invalid hash ${hash}`)
if (this.#busy) {
console.log('NanoPowGpu is busy. Retrying search...')
return new Promise(resolve => {
setTimeout(async (): Promise<void> => {
- const result = this.search(hash, options)
+ const result = this.work_generate(hash, options)
resolve(result)
}, 100)
})
}
this.#busy = true
- const threshold = (typeof options?.threshold !== 'number' || options.threshold < 0x0 || options.threshold > 0xffffffff)
- ? 0xfffffff8
+ const threshold = (typeof options?.threshold !== 'bigint' || options.threshold < 1n || options.threshold > 0xffffffffffffffffn)
+ ? 0xfffffff800000000n
: options.threshold
const effort = (typeof options?.effort !== 'number' || options.effort < 0x1 || options.effort > 0x20)
? 0x800
this.#debug = !!(options?.debug)
if (this.#debug) console.log('NanoPowGpu.search()')
if (this.#debug) console.log('blockhash', hash)
- if (this.#debug) console.log('search options', JSON.stringify(options))
+ if (this.#debug) console.log('search options', JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
// Ensure WebGPU is initialized before calculating
let loads = 0
let times = []
let start = performance.now()
let nonce = 0n
+ let result = 0n
do {
start = performance.now()
const random0 = Math.floor(Math.random() * 0xffffffff)
const data = await this.#dispatch(this.#searchPipeline, seed, hash, threshold, effort)
const found = !!data.getUint32(0)
nonce = data.getBigUint64(8, true)
- if (this.#debug) console.log('result', data.getBigUint64(16).toString(16).padStart(16, '0'))
+ result = data.getBigUint64(16, true)
this.#busy = !found
times.push(performance.now() - start)
} while (this.#busy)
if (this.#debug) this.#logAverages(times)
if (this.#debug) console.log('nonce', nonce, nonce.toString(16).padStart(16, '0'))
- return nonce.toString(16).padStart(16, '0')
+ if (this.#debug) console.log('result', result, result.toString(16).padStart(16, '0'))
+ return {
+ hash,
+ work: nonce.toString(16).padStart(16, '0'),
+ difficulty: result.toString(16).padStart(16, '0')
+ }
}
/**
* @param {NanoPowOptions} options - Options used to configure search execution
*/
static async validate (work: string, hash: string, options?: NanoPowOptions): Promise<boolean> {
+ if (options?.threshold != null) {
+ options.threshold = BigInt(`0x${options.threshold.toString(16) ?? '0'}00000000`)
+ }
+ const result = await this.work_validate(work, hash, options)
+ return (options?.threshold != null)
+ ? result.valid === '1'
+ : result.valid_all === '1'
+ }
+
+ /**
+ * 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 (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new TypeError(`Invalid work ${work}`)
if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new TypeError(`Invalid hash ${hash}`)
if (this.#busy) {
console.log('NanoPowGpu is busy. Retrying validate...')
return new Promise(resolve => {
setTimeout(async (): Promise<void> => {
- const result = this.validate(work, hash, options)
+ const result = this.work_validate(work, hash, options)
resolve(result)
}, 100)
})
}
this.#busy = true
- const threshold = (typeof options?.threshold !== 'number' || options.threshold < 0x0 || options.threshold > 0xffffffff)
- ? 0xfffffff8
+ const threshold = (typeof options?.threshold !== 'bigint' || options.threshold < 1n || options.threshold > 0xffffffffffffffffn)
+ ? 0xfffffff800000000n
: options.threshold
this.#debug = !!(options?.debug)
if (this.#debug) console.log('NanoPowGpu.validate()')
if (this.#debug) console.log('blockhash', hash)
- if (this.#debug) console.log('validate options', JSON.stringify(options))
+ if (this.#debug) console.log('validate options', JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
// Ensure WebGPU is initialized before calculating
let loads = 0
throw new Error(`WebGPU device failed to load.`)
}
+ let result = 0n
+ let nonce = 0n
+
const seed = BigInt(`0x${work}`)
if (this.#debug) console.log('work', work)
const data = await this.#dispatch(this.#validatePipeline, seed, hash, threshold, 1)
- const found = !!data.getUint32(0)
- const nonce = data.getBigUint64(8, true)
- if (this.#debug) console.log('result', data.getBigUint64(16).toString(16).padStart(16, '0'))
+ nonce = data.getBigUint64(8, true)
+ result = data.getBigUint64(16, true)
this.#busy = false
if (this.#debug) console.log('nonce', nonce, nonce.toString(16).padStart(16, '0'))
- if (found && work !== nonce.toString(16).padStart(16, '0')) throw new Error(`Nonce (${nonce.toString(16).padStart(16, '0')}) found but does not match work (${work})`)
- return found
+ if (this.#debug) console.log('result', result, result.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?.threshold != null) response.valid = (result >= threshold) ? '1' : '0'
+ return response
}
}
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { NanoPow, NanoPowGl, NanoPowGpu } from "./lib"
+import { NanoPow, NanoPowGl, NanoPowGpu } from './lib'
export { NanoPow, NanoPowGl, NanoPowGpu }
export default NanoPow
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
// SPDX-License-Identifier: GPL-3.0-or-later
-import "@webgpu/types"
+import '@webgpu/types'
+
+/**
+* Used by work server for inbound requests to `work_generate`.
+*
+* @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
+*/
+type WorkGenerateRequest = {
+ action: 'work_generate'
+ hash: string
+ difficulty?: string
+}
+
+/**
+* Used by work server for outbound responses to `work_generate`.
+*
+* @param {string} hash - Block hash used to generate or validate work
+* @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
+*/
+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 = {
+ action: 'work_validate'
+ hash: string
+ work: string
+ difficulty?: string
+}
+
+/**
+* Used by work server for outbound responses to `work_validate`.
+*
+* @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} 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
+*/
+type WorkValidateResponse = {
+ hash: string
+ work: string
+ difficulty: string
+ valid?: '0' | '1'
+ valid_all: '0' | '1'
+ valid_receive: '0' | '1'
+}
export declare const NanoPowGlDownsampleShader: string
export declare const NanoPowGlDrawShader: string
export type NanoPowOptions = {
debug?: boolean
effort?: number
- threshold?: number
+ threshold?: bigint | number
}
/**
* Nano proof-of-work using WebGL 2.0.
*/
export declare class NanoPowGl {
- /** Compile */
+ #private
+ /** Drawing buffer width in pixels. */
+ static get size (): number | undefined
+ /**
+ * 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
+ * @param {NanoPowOptions} options - Used to configure search execution
*/
static search (hash: string, options?: NanoPowOptions): Promise<string>
/**
+ * 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 {NanoPowOptions} options - Options used to configure search execution
*/
static validate (work: string, hash: string, options?: NanoPowOptions): Promise<boolean>
+ /**
+ * 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>
}
/**
* Nano proof-of-work using WebGPU.
*/
export declare class NanoPowGpu {
+ #private
static init (): Promise<void>
static setup (): void
static reset (): void
*/
static search (hash: string, options?: NanoPowOptions): Promise<string>
/**
+ * 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 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 {NanoPowOptions} options - Options used to configure search execution
*/
static validate (work: string, hash: string, options?: NanoPowOptions): Promise<boolean>
+ /**
+ * 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>
}
8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01
204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA
281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117
+7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090
0000000000000000000000000000000000000000000000000000000000000000
BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E
BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6
32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F
+39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150
+9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F
}
const title = type === 'WebGPU'
? `NanoPow (${type}) | Effort: ${effort} | Dispatch: ${(0x100 * effort) ** 2} | Threads: ${8 * 8 * (0x100 * effort) ** 2}`
- : `NanoPow (${type}) | Effort: ${effort} | Frame: ${NanoPowGl.size} | Pixels: ${NanoPowGl.size ** 2}`
+ : `NanoPow (${type}) | Effort: ${effort} | Pixels: ${NanoPowGl.size}`
return {
[title]: {
count: count,
const expect = []
let result
+ // PASS
result = await NP.validate('47c83266398728cf', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', { debug: isDebug })
console.log(`validate() output for good nonce 1 is ${result === true ? 'correct' : 'incorrect'}`)
expect.push(result === true)
console.log(`validate() output for colliding nonce is ${result === true ? 'correct' : 'incorrect'}`)
expect.push(result === true)
+ result = await NP.validate('6866c1ac3831a891', '7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090', { threshold: 0xfffffe00, debug: isDebug })
+ console.log(`validate() output for good receive threshold nonce is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result === true)
+
+ // XFAIL
result = await NP.validate('0000000000000000', '0000000000000000000000000000000000000000000000000000000000000000', { debug: isDebug })
console.log(`validate() output for bad nonce 1 is ${result === false ? 'correct' : 'incorrect'}`)
expect.push(result === false)
console.log(`validate() output for slightly wrong nonce is ${result === false ? 'correct' : 'incorrect'}`)
expect.push(result === false)
+ result = await NP.validate('7d903b18d03f9820', '39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150', { threshold: 0xfffffe00, debug: isDebug })
+ console.log(`validate() output for bad receive threshold nonce is ${result === false ? 'correct' : 'incorrect'}`)
+ expect.push(result === false)
+
+ result = await NP.validate('e45835c3b291c3d1', '9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F', { threshold: 0xffffffff, debug: isDebug })
+ console.log(`validate() output for send threshold nonce that does not meet custom threshold is ${result === false ? 'correct' : 'incorrect'}`)
+ expect.push(result === false)
try {
if (!expect.every(result => result)) throw new Error(`Validation is not working`)