From d682c1a646947d07874b1a6da68e41af88124687 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Thu, 20 Mar 2025 14:56:55 -0700 Subject: [PATCH] Set up for v4 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. --- .gitignore | 4 + CHANGELOG.md | 143 +++++++++++++++++ esbuild.mjs | 21 ++- package-lock.json | 24 +-- package.json | 7 +- src/bin/{cli.js => cli.ts} | 14 +- .../{gl-downsample.ts => gl-downsample.frag} | 5 +- src/lib/gl/{gl-draw.ts => gl-draw.frag} | 17 +-- src/lib/gl/gl-vertex.ts | 18 --- src/lib/gl/gl-vertex.vert | 17 +++ src/lib/gl/index.ts | 144 +++++++++++++----- src/lib/gpu/compute.wgsl | 13 +- src/lib/gpu/index.ts | 86 ++++++++--- src/main.ts | 2 +- src/types.d.ts | 109 ++++++++++++- test/blockhashes.txt | 3 + test/index.html | 15 +- 17 files changed, 519 insertions(+), 123 deletions(-) rename src/bin/{cli.js => cli.ts} (96%) rename src/lib/gl/{gl-downsample.ts => gl-downsample.frag} (93%) rename src/lib/gl/{gl-draw.ts => gl-draw.frag} (94%) delete mode 100644 src/lib/gl/gl-vertex.ts create mode 100644 src/lib/gl/gl-vertex.vert diff --git a/.gitignore b/.gitignore index 657bd01..a3dac34 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ node_modules/ build/ dist/ types/ + +# IDE +.vs/ +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 9589ab8..cb1a02b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -221,3 +221,146 @@ Add Typescript typings for new WebGL types. 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: } 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` 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/ diff --git a/esbuild.mjs b/esbuild.mjs index 2ccecf6..14a0ba3 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -8,15 +8,32 @@ await build({ 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' +}) diff --git a/package-lock.json b/package-lock.json index 4011896..bb9e90f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,9 @@ }, "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": { @@ -526,9 +526,9 @@ } }, "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" }, @@ -618,9 +618,9 @@ } }, "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": { @@ -894,13 +894,13 @@ } }, "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" diff --git a/package.json b/package.json index c0c0458..5a39e32 100644 --- a/package.json +++ b/package.json @@ -45,14 +45,13 @@ "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 \\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", diff --git a/src/bin/cli.js b/src/bin/cli.ts similarity index 96% rename from src/bin/cli.js rename to src/bin/cli.ts index 4f9b834..7c31441 100755 --- a/src/bin/cli.js +++ b/src/bin/cli.ts @@ -6,9 +6,9 @@ import * as fs from 'node:fs/promises' 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 @@ -46,9 +46,9 @@ Full documentation: 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) @@ -115,13 +115,11 @@ if (hashes.length === 0) { 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() @@ -135,7 +133,7 @@ if (hashes.length === 0) { 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] = { diff --git a/src/lib/gl/gl-downsample.ts b/src/lib/gl/gl-downsample.frag similarity index 93% rename from src/lib/gl/gl-downsample.ts rename to src/lib/gl/gl-downsample.frag index 9f15b47..d9ab533 100644 --- a/src/lib/gl/gl-downsample.ts +++ b/src/lib/gl/gl-downsample.frag @@ -1,8 +1,8 @@ +#version 300 es +#pragma vscode_glsllint_stage: frag //! SPDX-FileCopyrightText: 2025 Chris Duncan //! 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 @@ -33,4 +33,3 @@ void main() { pixel = texture(src, blockCoord + vec2(texel.x, texel.y)); nonce = pixel.x == 0u ? nonce : pixel; } -` diff --git a/src/lib/gl/gl-draw.ts b/src/lib/gl/gl-draw.frag similarity index 94% rename from src/lib/gl/gl-draw.ts rename to src/lib/gl/gl-draw.frag index ca113f5..bed5396 100644 --- a/src/lib/gl/gl-draw.ts +++ b/src/lib/gl/gl-draw.frag @@ -1,9 +1,9 @@ +#version 300 es +#pragma vscode_glsllint_stage: frag //! SPDX-FileCopyrightText: 2025 Chris Duncan //! SPDX-FileContributor: Ben Green //! 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 @@ -14,11 +14,11 @@ out uvec4 nonce; // 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 @@ -227,9 +227,9 @@ void main() { 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 @@ -237,4 +237,3 @@ void main() { discard; } } -` diff --git a/src/lib/gl/gl-vertex.ts b/src/lib/gl/gl-vertex.ts deleted file mode 100644 index 9378c78..0000000 --- a/src/lib/gl/gl-vertex.ts +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Chris Duncan -// SPDX-FileContributor: Ben Green -// 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; -} -` diff --git a/src/lib/gl/gl-vertex.vert b/src/lib/gl/gl-vertex.vert new file mode 100644 index 0000000..9491409 --- /dev/null +++ b/src/lib/gl/gl-vertex.vert @@ -0,0 +1,17 @@ +#version 300 es +#pragma vscode_glsllint_stage: vert +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-FileContributor: Ben Green +//! 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; +} diff --git a/src/lib/gl/index.ts b/src/lib/gl/index.ts index 91d5e66..a1d45ee 100644 --- a/src/lib/gl/index.ts +++ b/src/lib/gl/index.ts @@ -2,12 +2,18 @@ //! SPDX-FileContributor: Ben Green //! 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 @@ -15,7 +21,8 @@ export class NanoPowGl { 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 @@ -34,12 +41,15 @@ export class NanoPowGl { 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 { if (this.#busy) return this.#busy = true @@ -166,8 +176,8 @@ export class NanoPowGl { /** 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 { @@ -175,6 +185,10 @@ export class NanoPowGl { } } + /** + * 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) @@ -286,7 +300,7 @@ export class NanoPowGl { * @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') @@ -320,10 +334,16 @@ export class NanoPowGl { 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') @@ -333,14 +353,28 @@ export class NanoPowGl { * 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 { + 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 { if (this.#busy) { console.log('NanoPowGl is busy. Retrying search...') return new Promise(resolve => { setTimeout(async (): Promise => { - const result = this.search(hash, options) + const result = this.work_generate(hash, options) resolve(result) }, 100) }) @@ -349,8 +383,8 @@ export class NanoPowGl { /** 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 @@ -358,7 +392,7 @@ export class NanoPowGl { 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) { @@ -382,8 +416,8 @@ export class NanoPowGl { 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) @@ -392,25 +426,32 @@ export class NanoPowGl { /** 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') + } } /** @@ -421,11 +462,28 @@ export class NanoPowGl { * @param {NanoPowOptions} options - Options used to configure search execution */ static async validate (work: string, hash: string, options?: NanoPowOptions): Promise { + 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 { if (this.#busy) { console.log('NanoPowGl is busy. Retrying validate...') return new Promise(resolve => { setTimeout(async (): Promise => { - const result = this.validate(work, hash, options) + const result = this.work_validate(work, hash, options) resolve(result) }, 100) }) @@ -435,13 +493,13 @@ export class NanoPowGl { /** 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') @@ -458,28 +516,40 @@ export class NanoPowGl { 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 } } diff --git a/src/lib/gpu/compute.wgsl b/src/lib/gpu/compute.wgsl index 7615e28..3b0c7ef 100644 --- a/src/lib/gpu/compute.wgsl +++ b/src/lib/gpu/compute.wgsl @@ -7,7 +7,7 @@ struct UBO { blockhash: array, 2>, seed: vec2, - threshold: u32 + threshold: vec2 }; @group(0) @binding(0) var ubo: UBO; @@ -118,7 +118,7 @@ fn search(@builtin(global_invocation_id) global_id: vec3, @builtin(local_in found = (local_id.x == 0u && atomicLoad(&work.found) != 0u); workgroupBarrier(); if (found) { return; } - main(global_id); + main(global_id, false); } /** @@ -127,7 +127,7 @@ fn search(@builtin(global_invocation_id) global_id: vec3, @builtin(local_in */ @compute @workgroup_size(1) fn validate(@builtin(global_invocation_id) global_id: vec3) { - main(global_id); + main(global_id, true); } /** @@ -140,7 +140,7 @@ fn validate(@builtin(global_invocation_id) global_id: vec3) { * 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) { +fn main(id: vec3, validate: bool) { /** * Initialize (nonce||blockhash) concatenation */ @@ -1662,10 +1662,11 @@ fn main(id: vec3) { * 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; } diff --git a/src/lib/gpu/index.ts b/src/lib/gpu/index.ts index 6f9759b..09f545e 100644 --- a/src/lib/gpu/index.ts +++ b/src/lib/gpu/index.ts @@ -3,11 +3,14 @@ /// 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 @@ -141,7 +144,7 @@ export class NanoPowGpu { console.table(averages) } - static async #dispatch (pipeline: GPUComputePipeline, seed: bigint, hash: string, threshold: number, passes: number): Promise { + static async #dispatch (pipeline: GPUComputePipeline, seed: bigint, hash: string, threshold: bigint, passes: number): Promise { 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 @@ -151,7 +154,7 @@ export class NanoPowGpu { 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) @@ -217,19 +220,33 @@ export class NanoPowGpu { * @param {NanoPowOptions} options - Used to configure search execution */ static async search (hash: string, options?: NanoPowOptions): Promise { + 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 { 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 => { - 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 @@ -237,7 +254,7 @@ export class NanoPowGpu { 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 @@ -254,6 +271,7 @@ export class NanoPowGpu { let times = [] let start = performance.now() let nonce = 0n + let result = 0n do { start = performance.now() const random0 = Math.floor(Math.random() * 0xffffffff) @@ -263,13 +281,18 @@ export class NanoPowGpu { 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') + } } /** @@ -280,25 +303,42 @@ export class NanoPowGpu { * @param {NanoPowOptions} options - Options used to configure search execution */ static async validate (work: string, hash: string, options?: NanoPowOptions): Promise { + 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 { 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 => { - 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 @@ -312,15 +352,25 @@ export class NanoPowGpu { 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 } } diff --git a/src/main.ts b/src/main.ts index 0b9e45c..414717a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! 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 diff --git a/src/types.d.ts b/src/types.d.ts index 14cc26e..fc5aa59 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,7 +1,67 @@ // SPDX-FileCopyrightText: 2025 Chris Duncan // 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 @@ -36,24 +96,41 @@ export type FBO = { 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 + /** + * 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 /** + * 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 + /** * Validates that a nonce satisfies Nano proof-of-work requirements. * * @param {string} work - Hexadecimal proof-of-work value to validate @@ -61,12 +138,21 @@ export declare class NanoPowGl { * @param {NanoPowOptions} options - Options used to configure search execution */ static validate (work: string, hash: string, options?: NanoPowOptions): Promise + /** + * 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 } /** * Nano proof-of-work using WebGPU. */ export declare class NanoPowGpu { + #private static init (): Promise static setup (): void static reset (): void @@ -78,6 +164,13 @@ export declare class NanoPowGpu { */ static search (hash: string, options?: NanoPowOptions): Promise /** + * 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 + /** * Validates that a nonce satisfies Nano proof-of-work requirements. * * @param {string} work - Hexadecimal proof-of-work value to validate @@ -85,4 +178,12 @@ export declare class NanoPowGpu { * @param {NanoPowOptions} options - Options used to configure search execution */ static validate (work: string, hash: string, options?: NanoPowOptions): Promise + /** + * 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 } diff --git a/test/blockhashes.txt b/test/blockhashes.txt index 55ad255..2740258 100644 --- a/test/blockhashes.txt +++ b/test/blockhashes.txt @@ -2,7 +2,10 @@ 8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01 204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA 281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117 +7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090 0000000000000000000000000000000000000000000000000000000000000000 BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6 32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F +39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150 +9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F diff --git a/test/index.html b/test/index.html index a78e58b..063eae5 100644 --- a/test/index.html +++ b/test/index.html @@ -52,7 +52,7 @@ SPDX-License-Identifier: GPL-3.0-or-later } 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, @@ -78,6 +78,7 @@ SPDX-License-Identifier: GPL-3.0-or-later 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) @@ -94,6 +95,11 @@ SPDX-License-Identifier: GPL-3.0-or-later 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) @@ -110,6 +116,13 @@ SPDX-License-Identifier: GPL-3.0-or-later 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`) -- 2.34.1