From: Chris Duncan Date: Fri, 21 Mar 2025 16:40:23 +0000 (-0700) Subject: Implement server. X-Git-Tag: v4.0.0~2 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=929074b01931e9d89a54f8d6b3de7383e34b4630;p=nano-pow.git Implement server. Add executable start a Node server, accept POST requests in same JSON format as Nano node specs, and process similarly to CLI using puppeteer. Remove migration compatibility layer introduced in v3.2.0. Extract help text to separate documentation file. Write basic test script to check server. Refactor threshold to expect 64-bit values only. Fix Typescript types. --- diff --git a/.gitignore b/.gitignore index a3dac34..905ca2a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ types/ # IDE .vs/ .vscode/ + +# Server process ID +server.pid diff --git a/docs/index.js b/docs/index.js new file mode 100644 index 0000000..da2b3f7 --- /dev/null +++ b/docs/index.js @@ -0,0 +1,38 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +export const cliHelp = `Usage: nano-pow [OPTION]... BLOCKHASH... +Generate work for BLOCKHASH, or multiple work values for BLOCKHASH(es) +BLOCKHASH is a 64-character hexadecimal string. Multiple blockhashes must be separated by whitespace or line breaks. +Prints a 16-character hexadecimal work value to standard output. If using --validate, prints 'true' or 'false' to standard output instead. + + -h, --help show this dialog + -d, --debug enable additional logging output + -j, --json format output as JSON + -e, --effort= increase demand on GPU processing + -t, --threshold= override the minimum threshold value + -v, --validate= check an existing work value instead of searching for one + +If validating a nonce, it must be a 16-character hexadecimal value. +Effort must be a decimal number between 1-32. +Threshold must be a hexadecimal string between 0-FFFFFFFF. + +Report bugs: +Full documentation: +` + +export const serverHelp = `Usage: Send POST request to server URL to generate or validate Nano proof-of-work + +Generate work for a BLOCKHASH with an optional DIFFICULTY: + curl -d '{ action: "work_generate", hash: BLOCKHASH, difficulty?: DIFFICULTY }' + +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) + +Report bugs: +Full documentation: +` diff --git a/package.json b/package.json index b77ea5b..3c346bc 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,9 @@ "url": "git+https://zoso.dev/nano-pow.git" }, "scripts": { - "build": "rm -rf {dist,types} && tsc && node esbuild.mjs" + "build": "rm -rf {dist,types} && tsc && node esbuild.mjs", + "start": "mkdir -p logs; node ./dist/bin/server.js > ./logs/nano-pow-server-$(date +%s).log 2>&1 & echo $! > server.pid", + "test": "./test/script.sh" }, "devDependencies": { "@types/node": "^22.13.11", diff --git a/src/bin/cli.ts b/src/bin/cli.ts index 138c742..2a3c135 100755 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -6,6 +6,7 @@ import * as fs from 'node:fs/promises' import * as readline from 'node:readline/promises' import * as puppeteer from 'puppeteer' +import { cliHelp } from '../../docs/index.js' const hashes: string[] = [] @@ -25,25 +26,7 @@ if (!process.stdin.isTTY) { const args = process.argv.slice(2) if ((hashes.length === 0 && args.length === 0) || (args.some(v => v === '--help' || v === '-h'))) { - console.log(`Usage: nano-pow [OPTION]... BLOCKHASH... -Generate work for BLOCKHASH, or multiple work values for BLOCKHASH(es) -BLOCKHASH is a 64-character hexadecimal string. Multiple blockhashes must be separated by whitespace or line breaks. -Prints a 16-character hexadecimal work value to standard output. If using --validate, prints 'true' or 'false' to standard output instead. - - -h, --help show this dialog - -d, --debug enable additional logging output - -j, --json format output as JSON - -e, --effort= increase demand on GPU processing - -t, --threshold= override the minimum threshold value - -v, --validate= check an existing work value instead of searching for one - -If validating a nonce, it must be a 16-character hexadecimal value. -Effort must be a decimal number between 1-32. -Threshold must be a hexadecimal string between 0-FFFFFFFF. - -Report bugs: -Full documentation: -`) + console.log(cliHelp) process.exit() } @@ -53,7 +36,7 @@ while (/^[0-9A-Fa-f]{64}$/.test(args[args.length - 1] ?? '')) { } hashes.push(...inArgs) -let fn = 'search' +let fn = 'work_generate' let work = '' let isJson = false const options = {} @@ -64,7 +47,7 @@ for (let i = 0; i < args.length; i++) { case ('-v'): { if (args[i + 1] == null) throw new Error('Missing argument for work validation') if (!/^[0-9A-Fa-f]{16}$/.test(args[i + 1])) throw new Error('Invalid work to validate') - fn = 'validate' + fn = 'work_validate' work = `'${args[i + 1]}', ` break } @@ -111,7 +94,7 @@ if (hashes.length === 0) { /** * Main */ -(async () => { +(async (): Promise => { const NanoPow = await fs.readFile(new URL('../main.min.js', import.meta.url), 'utf-8') const browser = await puppeteer.launch({ headless: true, @@ -127,7 +110,7 @@ if (hashes.length === 0) { const cliPage = `${import.meta.dirname}/cli.html` await fs.writeFile(cliPage, '') await page.goto(import.meta.resolve('./cli.html')) - await page.waitForFunction(async () => { + await page.waitForFunction(async (): Promise => { return await navigator.gpu.requestAdapter() }) @@ -137,9 +120,9 @@ if (hashes.length === 0) { const hashes = ["${hashes.join('","')}"] for (const hash of hashes) { try { - const work = await NanoPow.${fn}(${work}hash, ${JSON.stringify(options)}) - window.results.push(work) - console.log(\`cli \${work}\`) + const result = await NanoPow.${fn}(${work}hash, ${JSON.stringify(options)}) + window.results.push(result) + console.log(\`cli \${JSON.stringify(result, null, 4)}\`) } catch (err) { console.error(\`cli \${err}\`) } @@ -150,30 +133,32 @@ if (hashes.length === 0) { const src = `sha256-${Buffer.from(hash).toString('base64')}` let start = performance.now() - page.on('console', async (msg) => { - const output = msg.text().split(' ') - if (output[0] === 'cli') { + page.on('console', async (msg): Promise => { + const output = msg.text().split(/^cli /) + if (output[0] === '') { if (output[1] === 'exit') { if (isJson) { - const results = await page.evaluate(() => { + const results = await page.evaluate((): any => { return (window as any).results }) - for (let i = 0; i < results.length; i++) { - results[i] = { - blockhash: hashes[i], - work: results[i] - } - } console.log(JSON.stringify(results, null, 4)) } const end = performance.now() if (options['debug']) console.log(end - start, 'ms total |', (end - start) / hashes.length, 'ms avg') await browser.close() } else if (!isJson) { - console.log(output[1]) + try { + console.log(JSON.parse(output[1])) + } catch (err) { + console.log(output[1]) + } } } else if (options['debug']) { - console.log(msg.text()) + try { + console.log(JSON.parse(msg.text())) + } catch (err) { + console.log(msg.text()) + } } }) start = performance.now() diff --git a/src/bin/server.ts b/src/bin/server.ts new file mode 100755 index 0000000..007abab --- /dev/null +++ b/src/bin/server.ts @@ -0,0 +1,200 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import * as http from 'node:http' +import * as fs from 'node:fs/promises' +import * as puppeteer from 'puppeteer' +import { serverHelp } from '../../docs/index.js' +import { NanoPowOptions, WorkGenerateRequest, WorkGenerateResponse, WorkValidateRequest, WorkValidateResponse } from '../types.js' + +const PORT = process.env.PORT || 3000 + +function log (...args) { + console.log(new Date(Date.now()).toLocaleString(), 'NanoPow', args) +} + +log('Starting server') + +const NanoPow = await fs.readFile(new URL('../main.min.js', import.meta.url), 'utf-8') + +// Launch puppeteer browser instance - Persistent instance +let browser: puppeteer.Browser +let page: puppeteer.Page + +async function work_generate (res: http.ServerResponse, json: WorkGenerateRequest): Promise { + if (!/^[0-9A-Fa-f]{64}$/.test(json.hash ?? '')) { + res.writeHead(400, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'Invalid hash. Must be a 64-character hex string.' })) + return + } + if (json.difficulty && !/^[1-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(json.difficulty)) { + res.writeHead(400, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'Invalid difficulty. Must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF.' })) + return + } + + try { + const result = await page.evaluate(async (args: WorkGenerateRequest): Promise => { + const options: NanoPowOptions = { + debug: true + } + if (args.difficulty) options.threshold = BigInt(`0x${args.difficulty}`) + // @ts-expect-error + return await window.NanoPow.work_generate(args.hash, options) + }, json) + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify(result)) + } catch (err) { + log('work_generate error:', err) + res.writeHead(500, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'work_generate failed' })) + } +} + +async function work_validate (res: http.ServerResponse, json: WorkValidateRequest): Promise { + if (!/^[0-9A-Fa-f]{64}$/.test(json.hash ?? '')) { + res.writeHead(400, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'Invalid hash. Must be a 64-character hex string.' })) + return + } + if (json.difficulty && !/^[1-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(json.difficulty)) { + res.writeHead(400, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'Invalid difficulty. Must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF.' })) + return + } + if (!/^[0-9A-Fa-f]{16}$/.test(json.work ?? '')) { + res.writeHead(400, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'Invalid work. Must be a 16-character hex string.' })) + return + } + + try { + const result: WorkValidateResponse = await page.evaluate(async (args: WorkValidateRequest): Promise => { + const options: NanoPowOptions = { + debug: true + } + if (args.difficulty) options.threshold = BigInt(`0x${args.difficulty}`) + //@ts-expect-error + return await window.NanoPow.work_validate(args.work, args.hash, options) + }, json) + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify(result)) + } catch (err) { + log('work_validate error:', err) + res.writeHead(500, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'work_validate failed' })) + } +} + +// Start server +(async (): Promise => { + // Initialize puppeteer + browser = await puppeteer.launch({ + headless: true, + args: [ + '--headless=new', + '--use-angle=vulkan', + '--enable-features=Vulkan', + '--disable-vulkan-surface', + '--enable-unsafe-webgpu' + ] + }) + page = await browser.newPage() + page.on('console', (msg): void => { + log(msg.text()) + }) + await fs.writeFile(`${import.meta.dirname}/server.html`, '') + await page.goto(import.meta.resolve('./server.html')) + await page.waitForFunction(async (): Promise => { + return await navigator.gpu.requestAdapter() + }) + + const inject = `${NanoPow};window.NanoPow=NanoPow;` + const hash = await crypto.subtle.digest('SHA-256', Buffer.from(inject, 'utf-8')) + const src = `sha256-${Buffer.from(hash).toString('base64')}` + + await page.setContent(` + + + + + + + `) + await fs.unlink(`${import.meta.dirname}/server.html`) + log('Puppeteer initialized') + + // Create server + const server = http.createServer(async (req, res): Promise => { + let data: Buffer[] = [] + if (req.method === 'POST') { + req.on('data', (chunk: Buffer): void => { + data.push(chunk) + }) + req.on('end', async (): Promise => { + let json + try { + json = JSON.parse(Buffer.concat(data).toString()) + } catch (err) { + log('JSON.parse error:', err) + log('Failed JSON:', json) + res.writeHead(400, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'Invalid data.' })) + return + } + switch (json.action) { + case ('work_generate'): { + await work_generate(res, json) + break + } + case ('work_validate'): { + await work_validate(res, json) + break + } + default: { + res.writeHead(400, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: `Invalid data.` })) + return + } + } + }) + } else { + res.writeHead(200, { 'Content-Type': 'text/plain' }) + res.end(serverHelp) + } + }) + + server.on('error', (e): void => { + log('Server error', e) + try { + shutdown() + } catch (err) { + log('Failed to shut down', err) + process.exit(1) + } + }) + + // Listen on configured port + server.listen(PORT, (): void => { + process.title = 'NanoPow Server' + log(`Server process ${process.pid} running at http://localhost:${PORT}/`) + }) + + // Shut down server gracefully when process is terminated + function shutdown (): void { + log('Shutdown signal received') + const kill = setTimeout((): never => { + log('Server unresponsive, forcefully stopped') + process.exit(1) + }, 10000) + server.close(async (): Promise => { + await page?.close() + await browser?.close() + clearTimeout(kill) + log('Server stopped') + process.exit(0) + }) + } + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) +})() diff --git a/src/lib/gl/index.ts b/src/lib/gl/index.ts index a1d45ee..c03954c 100644 --- a/src/lib/gl/index.ts +++ b/src/lib/gl/index.ts @@ -349,20 +349,6 @@ export class NanoPowGl { 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 - 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. * @@ -370,6 +356,7 @@ export class NanoPowGl { * @param {NanoPowOptions} options - 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 Error(`Invalid hash ${hash}`) if (this.#busy) { console.log('NanoPowGl is busy. Retrying search...') return new Promise(resolve => { @@ -381,8 +368,13 @@ export class NanoPowGl { } this.#busy = true - /** Process user input */ - if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`) + if (typeof options?.threshold === 'string') { + try { + options.threshold = BigInt(`0x${options.threshold}`) + } catch (err) { + throw new TypeError(`Invalid threshold ${options.threshold}`) + } + } const threshold = (typeof options?.threshold !== 'bigint' || options.threshold < 1n || options.threshold > 0xffffffffffffffffn) ? 0xfffffff800000000n : options.threshold @@ -390,7 +382,7 @@ export class NanoPowGl { ? this.#cores : options.effort this.#debug = !!(options?.debug) - if (this.#debug) console.log('NanoPowGl.search()') + 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)) @@ -454,23 +446,6 @@ export class NanoPowGl { } } - /** - * 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 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. * @@ -479,6 +454,8 @@ export class NanoPowGl { * @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 Error(`Invalid work ${work}`) + if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`) if (this.#busy) { console.log('NanoPowGl is busy. Retrying validate...') return new Promise(resolve => { @@ -490,14 +467,18 @@ export class NanoPowGl { } this.#busy = true - /** 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}`) + if (typeof options?.threshold === 'string') { + try { + options.threshold = BigInt(`0x${options.threshold}`) + } catch (err) { + throw new TypeError(`Invalid threshold ${options.threshold}`) + } + } 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('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)) diff --git a/src/lib/gpu/index.ts b/src/lib/gpu/index.ts index 09f545e..ba4cb7a 100644 --- a/src/lib/gpu/index.ts +++ b/src/lib/gpu/index.ts @@ -213,20 +213,6 @@ export class NanoPowGpu { return data } - /** - * 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 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. * @@ -245,6 +231,14 @@ export class NanoPowGpu { }) } this.#busy = true + + if (typeof options?.threshold === 'string') { + try { + options.threshold = BigInt(`0x${options.threshold}`) + } catch (err) { + throw new TypeError(`Invalid threshold ${options.threshold}`) + } + } const threshold = (typeof options?.threshold !== 'bigint' || options.threshold < 1n || options.threshold > 0xffffffffffffffffn) ? 0xfffffff800000000n : options.threshold @@ -252,7 +246,7 @@ export class NanoPowGpu { ? 0x800 : options.effort * 0x100 this.#debug = !!(options?.debug) - if (this.#debug) console.log('NanoPowGpu.search()') + if (this.#debug) console.log('NanoPowGpu.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)) @@ -295,23 +289,6 @@ export class NanoPowGpu { } } - /** - * 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 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. * @@ -332,11 +309,19 @@ export class NanoPowGpu { }) } this.#busy = true + + if (typeof options?.threshold === 'string') { + try { + options.threshold = BigInt(`0x${options.threshold}`) + } catch (err) { + throw new TypeError(`Invalid threshold ${options.threshold}`) + } + } 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('NanoPowGpu.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)) diff --git a/src/types.d.ts b/src/types.d.ts index fc5aa59..1483ac1 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -91,12 +91,12 @@ export type FBO = { * * @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 {number} [threshold=0xfffffff8] - Minimum value result of `BLAKE2b(nonce||blockhash) << 0x32`. Default: 0xFFFFFFF8 +* @param {bigint|string} [threshold=0xfffffff800000000] - Minimum value result of `BLAKE2b(nonce||blockhash)`. Default: 0xFFFFFFF800000000 */ export type NanoPowOptions = { debug?: boolean effort?: number - threshold?: bigint | number + threshold?: bigint | string } /** @@ -105,7 +105,7 @@ export type NanoPowOptions = { export declare class NanoPowGl { #private /** Drawing buffer width in pixels. */ - static get size (): number | undefined + static get size (): number /** * Constructs canvas, gets WebGL context, initializes buffers, and compiles * shaders. diff --git a/test/index.html b/test/index.html index 063eae5..d063e1f 100644 --- a/test/index.html +++ b/test/index.html @@ -72,84 +72,97 @@ SPDX-License-Identifier: GPL-3.0-or-later export async function run (threshold, size, effort, isOutputShown, isGlForced, isDebug) { const NP = isGlForced ? NanoPowGl : NanoPow const type = (NP === NanoPowGpu) ? 'WebGPU' : (NP === NanoPowGl) ? 'WebGL' : 'unknown API' - document.getElementById('status').innerHTML = `TESTING IN PROGRESS 0/${size}` - console.log(`%cNanoPow`, 'color:green', 'Checking validate()') + console.log(`%cNanoPow`, 'color:green', 'Checking validation against known values') 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) + 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.validate('4a8fb104eebbd336', '8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01', { debug: isDebug }) - console.log(`validate() output for good nonce 2 is ${result === true ? 'correct' : 'incorrect'}`) - expect.push(result === true) + 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.validate('326f310d629a8a98', '204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA', { threshold: 0xffffffff, debug: isDebug }) - console.log(`validate() output for good max threshold nonce is ${result === true ? 'correct' : 'incorrect'}`) - expect.push(result === true) + result = await NP.work_validate('326f310d629a8a98', '204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA', { threshold: 0xffffffff00000000n, debug: isDebug }) + result = result.valid === '1' && result.valid_all === '1' && result.valid_receive === '1' + console.log(`work_validate() output for good max threshold nonce is ${result === true ? 'correct' : 'incorrect'}`) + expect.push(result) - result = await NP.validate('c5d5d6f7c5d6ccd1', '281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117', { debug: isDebug }) - console.log(`validate() output for colliding nonce is ${result === true ? 'correct' : 'incorrect'}`) - expect.push(result === true) + 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.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) + result = await NP.work_validate('6866c1ac3831a891', '7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090', { threshold: 0xfffffe0000000000n, debug: isDebug }) + result = result.valid === '1' && result.valid_all === '0' && result.valid_receive === '1' + console.log(`work_validate() output for good receive threshold nonce is ${result === true ? 'correct' : 'incorrect'}`) + expect.push(result) // 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) + 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.validate('c5d5d6f7c5d6ccd1', 'BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E', { debug: isDebug }) - console.log(`validate() output for bad nonce 2 is ${result === false ? 'correct' : 'incorrect'}`) - expect.push(result === false) + 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) - result = await NP.validate('ae238556213c3624', 'BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6', { threshold: 0xffffffff, debug: isDebug }) - console.log(`validate() output for bad max threshold nonce is ${result === false ? 'correct' : 'incorrect'}`) - expect.push(result === false) + result = await NP.work_validate('ae238556213c3624', 'BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6', { threshold: 0xffffffff00000000n, debug: isDebug }) + result = result.valid === '0' && result.valid_all === '0' && result.valid_receive === '1' + console.log(`work_validate() output for bad max threshold nonce is ${result === true ? 'correct' : 'incorrect'}`) + expect.push(result) - result = await NP.validate('29a9ae0236990e2e', '32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F', { debug: isDebug }) - console.log(`validate() output for slightly wrong nonce is ${result === false ? 'correct' : 'incorrect'}`) - expect.push(result === false) + 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) - 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.work_validate('7d903b18d03f9820', '39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150', { threshold: 0xfffffe0000000000n, debug: isDebug }) + result = result.valid === '0' && result.valid_all === '0' && result.valid_receive === '0' + console.log(`work_validate() output for bad receive threshold nonce is ${result === true ? 'correct' : 'incorrect'}`) + expect.push(result) - 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) + result = await NP.work_validate('e45835c3b291c3d1', '9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F', { threshold: 0xffffffff00000000n, debug: isDebug }) + result = result.valid === '0' && result.valid_all === '1' && result.valid_receive === '1' + console.log(`work_validate() output for send threshold nonce that does not meet custom threshold 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}
` console.error(err) return } + document.getElementById('status').innerHTML = `TESTING IN PROGRESS 0/${size}` console.log(`%cNanoPow (${type})`, 'color:green', `Calculate proof-of-work for ${size} unique send block hashes`) const times = [] for (let i = 0; i < size; i++) { document.getElementById('status').innerHTML = `TESTING IN PROGRESS ${i}/${size}
` const hash = random() - let work = null + let result = null const start = performance.now() try { - work = await NP.search(hash, { threshold, effort, debug: isDebug }) + result = await NP.work_generate(hash, { threshold, effort, debug: isDebug }) } catch (err) { document.getElementById('output').innerHTML += `Error: ${err.message}
` console.error(err) return } const end = performance.now() - const isValid = (await NP.validate(work, hash, { threshold, debug: isDebug })) ? 'VALID' : 'INVALID' + const check = await NP.work_validate(result.work, result.hash, { threshold, debug: isDebug }) + const isValid = (result.hash === hash && check.valid === '1') ? 'VALID' : 'INVALID' times.push(end - start) - const msg = `${isValid} [${work}] ${hash} (${end - start} ms)` + const msg = `${isValid} [${result.work}] ${result.hash} (${end - start} ms)` if (isOutputShown) document.getElementById('output').innerHTML += `${msg}
` } document.getElementById('output').innerHTML += `
` @@ -167,7 +180,7 @@ SPDX-License-Identifier: GPL-3.0-or-later validation.innerText = '⏳' if (work.value.length === 16 && hash.value.length === 64) { const NP = isGlForced ? NanoPowGl : NanoPow - NP.validate(work.value, hash.value, { threshold: `0x${+threshold.value}` }) + NP.work_validate(work.value, hash.value, { threshold: threshold.value }) .then(result => { validation.innerText = result ? '✔️' @@ -189,12 +202,16 @@ SPDX-License-Identifier: GPL-3.0-or-later const isOutputShown = document.getElementById('isOutputShown') const isGlForced = document.getElementById('isGlForced') const isDebug = document.getElementById('isDebug') - run(+`0x${threshold.value}`, +size.value, +effort.value, isOutputShown.checked, isGlForced.checked, isDebug.checked) + run(threshold.value, +size.value, +effort.value, isOutputShown.checked, isGlForced.checked, isDebug.checked) } document.getElementById('btnStartTest').addEventListener('click', startTest) document.getElementById('effort').value = Math.max(1, Math.floor(navigator.hardwareConcurrency)) - + @@ -206,12 +223,12 @@ SPDX-License-Identifier: GPL-3.0-or-later

Times below are in milliseconds and are summarized by various averaging methods.

Level of Effort depends on hardware and does not guarantee faster results.


- - + +
- + - +
diff --git a/test/script.sh b/test/script.sh new file mode 100755 index 0000000..e83e34f --- /dev/null +++ b/test/script.sh @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2025 Chris Duncan +# SPDX-License-Identifier: GPL-3.0-or-later + +npm start +sleep 2s + +printf '\nGet documentation\n' +curl localhost:3000 + +printf '\nExpect error. Server should not crash when bad data is received like missing end quote\n' +curl -d '{ "action": "work_validate", "work": "47c83266398728cf", "hash: "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D }' localhost:3000 + +printf '\nValidate good hashes\n' +curl -d '{ "action": "work_validate", "work": "47c83266398728cf", "hash": "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D" }' localhost:3000 +curl -d '{ "action": "work_validate", "work": "4a8fb104eebbd336", "hash": "8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01" }' localhost:3000 +curl -d '{ "action": "work_validate", "work": "326f310d629a8a98", "hash": "204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA", "difficulty": "ffffffff00000000" }' localhost:3000 +curl -d '{ "action": "work_validate", "work": "c5d5d6f7c5d6ccd1", "hash": "281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117" }' localhost:3000 +curl -d '{ "action": "work_validate", "work": "6866c1ac3831a891", "hash": "7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090", "difficulty": "fffffe0000000000" }' localhost:3000 + +printf '\nValidate bad hashes\n' +curl -d '{ "action": "work_validate", "work": "0000000000000000", "hash": "0000000000000000000000000000000000000000000000000000000000000000" }' localhost:3000 +curl -d '{ "action": "work_validate", "work": "c5d5d6f7c5d6ccd1", "hash": "BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E" }' localhost:3000 +curl -d '{ "action": "work_validate", "work": "ae238556213c3624", "hash": "BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6", "difficulty": "ffffffff00000000" }' localhost:3000 +curl -d '{ "action": "work_validate", "work": "29a9ae0236990e2e", "hash": "32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F" }' localhost:3000 +curl -d '{ "action": "work_validate", "work": "7d903b18d03f9820", "hash": "39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150", "difficulty": "fffffe0000000000" }' localhost:3000 +curl -d '{ "action": "work_validate", "work": "e45835c3b291c3d1", "hash": "9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F", "difficulty": "ffffffff00000000" }' localhost:3000 + + +printf '\nGenerate\n' +curl -d '{ "action": "work_generate", "hash": "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D" }' localhost:3000 +curl -d '{ "action": "work_generate", "hash": "204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA", "difficulty": "ffffffff00000000" }' localhost:3000 +curl -d '{ "action": "work_generate", "hash": "7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090", "difficulty": "fffffe0000000000" }' localhost:3000 +kill $(cat server.pid) && rm server.pid