]> zoso.dev Git - nano-pow.git/commitdiff
Implement server.
authorChris Duncan <chris@zoso.dev>
Fri, 21 Mar 2025 16:40:23 +0000 (09:40 -0700)
committerChris Duncan <chris@zoso.dev>
Fri, 21 Mar 2025 16:40:23 +0000 (09:40 -0700)
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.

.gitignore
docs/index.js [new file with mode: 0644]
package.json
src/bin/cli.ts
src/bin/server.ts [new file with mode: 0755]
src/lib/gl/index.ts
src/lib/gpu/index.ts
src/types.d.ts
test/index.html
test/script.sh [new file with mode: 0755]

index a3dac34d01e1565e8fcd09a7ce082f34dcbc19d6..905ca2a207f7320ea93106fa8fa92ba6dc695824 100644 (file)
@@ -23,3 +23,6 @@ types/
 # IDE\r
 .vs/\r
 .vscode/\r
+\r
+# Server process ID\r
+server.pid\r
diff --git a/docs/index.js b/docs/index.js
new file mode 100644 (file)
index 0000000..da2b3f7
--- /dev/null
@@ -0,0 +1,38 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! 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=<value>        increase demand on GPU processing
+  -t, --threshold=<value>     override the minimum threshold value
+  -v, --validate=<value>      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: <bug-nano-pow@zoso.dev>
+Full documentation: <https://www.npmjs.com/package/nano-pow>
+`
+
+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: <bug-nano-pow@zoso.dev>
+Full documentation: <https://www.npmjs.com/package/nano-pow>
+`
index b77ea5b181437c70a025d178318216674658a44b..3c346bcaf29585101b2f84e591957e85a6d4e397 100644 (file)
@@ -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",
index 138c7425fce8c6fdf2714349794a0072a9fa7222..2a3c135ad711bf6a91cce1a3212f30ced9559cb0 100755 (executable)
@@ -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=<value>        increase demand on GPU processing
-  -t, --threshold=<value>     override the minimum threshold value
-  -v, --validate=<value>      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: <bug-nano-pow@zoso.dev>
-Full documentation: <https://www.npmjs.com/package/nano-pow>
-`)
+       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<void> => {
        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<GPUAdapter | null> => {
                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<void> => {
+               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 (executable)
index 0000000..007abab
--- /dev/null
@@ -0,0 +1,200 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! 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<void> {
+       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<WorkGenerateResponse> => {
+                       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<void> {
+       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<WorkValidateResponse> => {
+                       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<void> => {
+       // 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<GPUAdapter | null> => {
+               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(`
+               <!DOCTYPE html>
+                       <head>
+                               <meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'; script-src '${src}';">
+                               <script type="module">${inject}</script>
+                       </head>
+               </html>
+       `)
+       await fs.unlink(`${import.meta.dirname}/server.html`)
+       log('Puppeteer initialized')
+
+       // Create server
+       const server = http.createServer(async (req, res): Promise<void> => {
+               let data: Buffer[] = []
+               if (req.method === 'POST') {
+                       req.on('data', (chunk: Buffer): void => {
+                               data.push(chunk)
+                       })
+                       req.on('end', async (): Promise<void> => {
+                               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<never> => {
+                       await page?.close()
+                       await browser?.close()
+                       clearTimeout(kill)
+                       log('Server stopped')
+                       process.exit(0)
+               })
+       }
+       process.on('SIGINT', shutdown)
+       process.on('SIGTERM', shutdown)
+})()
index a1d45eefde84a87ddd9689bb9c6e30e2ae584092..c03954cb71f3a92fec9b42542c67ccb09eb508b4 100644 (file)
@@ -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<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.
        *
@@ -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<WorkGenerateResponse> {
+               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<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.
        *
@@ -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<WorkValidateResponse> {
+               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))
 
index 09f545e48d36cb60793357b2e2c748ab5cd82dfa..ba4cb7a79e3ac4d1c88ed0b6a09ff8934f6cad16 100644 (file)
@@ -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<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.
        *
@@ -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<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.
        *
@@ -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))
 
index fc5aa59f2c28bce58ff8eed93083d2d3bcbb12d3..1483ac10e471bcf41118313dd15d99ae56047479 100644 (file)
@@ -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.
index 063eae5680de4496beba2e503c18dc43c8039767..d063e1f0945e314ec13871d29556375a931ffd37 100644 (file)
@@ -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}<br/>`
                                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}<br/>`
                                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}<br/>`
                                        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}<br/>`
                        }
                        document.getElementById('output').innerHTML += `<hr/>`
@@ -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))
        </script>
-       <style>body{background:black;color:white;}a{color:darkcyan;}input[type=number]{width:5em;}span{margin:0.5em;}</style>
+       <style>
+               body{background:black;color:white;}a{color:darkcyan;}input[type=number]{width:5em;}span{margin:0.5em;}
+               label.hex::after{color:grey;content:'0x';display:inline-block;font-size:90%;left:0.5em;position:relative;width:0;}
+               label.hex+input{padding-left:1.25em;}
+       </style>
 </head>
 
 <body>
@@ -206,12 +223,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
        <p>Times below are in milliseconds and are summarized by various averaging methods.</p>
        <p>Level of Effort depends on hardware and does not guarantee faster results.</p>
        <hr />
-       <label for="threshold">Threshold 0x</label>
-       <input id="threshold" type="text" value="FFFFFFF8" />
+       <label for="threshold" class="hex">Threshold</label>
+       <input id="threshold" type="text" value="FFFFFFF800000000" />
        <hr />
-       <label for="work">Validate Work</label>
+       <label for="work" class="hex">Validate Work</label>
        <input id="work" type="text" />
-       <label for="hash">Hash</label>
+       <label for="hash" class="hex">Hash</label>
        <input id="hash" type="text" />
        <span id="validation"></span>
        <hr />
diff --git a/test/script.sh b/test/script.sh
new file mode 100755 (executable)
index 0000000..e83e34f
--- /dev/null
@@ -0,0 +1,33 @@
+# SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+# 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