]> zoso.dev Git - nano-pow.git/commitdiff
Overhaul server to consolidate work methods into one function. Overhaul CLI to spin...
authorChris Duncan <chris@zoso.dev>
Thu, 27 Mar 2025 21:03:44 +0000 (14:03 -0700)
committerChris Duncan <chris@zoso.dev>
Thu, 27 Mar 2025 21:03:44 +0000 (14:03 -0700)
docs/index.js [deleted file]
esbuild.mjs
src/bin/cli.ts
src/bin/server.ts
src/lib/gl/index.ts
src/lib/gpu/index.ts
src/types.d.ts
test/blockhashes.txt
test/index.html
test/script.sh
tsconfig.json

diff --git a/docs/index.js b/docs/index.js
deleted file mode 100644 (file)
index 5e10410..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-//! 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                  gather all results and output them at once 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 1-FFFFFFFFFFFFFFFF.
-
-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 14a0ba3f5dd67a7cccdf6f2b502dee4cef8d3d88..2d9ade2c36108d5e6ca5de46f3ce2192fe00347b 100644 (file)
@@ -30,10 +30,12 @@ await build({
        bundle: false,
        platform: 'node',
        entryPoints: [
-               './src/bin/*.ts'
+               './src/bin/cli.ts',
+               './src/bin/server.ts'
        ],
        format: 'esm',
        legalComments: 'inline',
        outdir: 'dist/bin',
+       packages: 'external',
        target: 'esnext'
 })
index 5181b65894e8efb8b97d472a82b070d9814c460e..d1eb8e486ff4334b622a879bb46ea44e1229b62c 100755 (executable)
@@ -1,34 +1,64 @@
 #!/usr/bin/env node
 //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 //! SPDX-License-Identifier: GPL-3.0-or-later
-/// <reference types="@webgpu/types" />
 
-import * as crypto from 'node:crypto'
-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'
+import type { WorkGenerateResponse, WorkValidateResponse } from '#types'
 
-const hashes: string[] = []
+process.title = 'NanoPow CLI'
+
+process.env.NANO_POW_DEBUG = ''
+process.env.NANO_POW_EFFORT = ''
+process.env.NANO_POW_PORT = '3000'
+
+function log (...args: any[]): void {
+       if (process.env.NANO_POW_DEBUG) console.log(new Date(Date.now()).toLocaleString(), 'NanoPow', args)
+}
 
+const hashes: string[] = []
 const stdinErrors: string[] = []
+
 if (!process.stdin.isTTY) {
        const stdin = readline.createInterface({
                input: process.stdin
        })
+       let i = 0
        for await (const line of stdin) {
+               i++
                if (/^[0-9A-Fa-f]{64}$/.test(line)) {
                        hashes.push(line)
                } else {
-                       stdinErrors.push(`Skipping invalid stdin input: ${line}`)
+                       stdinErrors.push(`Skipping invalid stdin input line ${i}`)
                }
        }
 }
 
 const args = process.argv.slice(2)
 if ((hashes.length === 0 && args.length === 0) || (args.some(v => v === '--help' || v === '-h'))) {
-       console.log(cliHelp)
-       process.exit()
+       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 the result as a Javascript object to standard output as soon as it is calculated.
+If using --batch, results are printed only after all BLOCKHASH(es) have be processed.
+If using --validate, results will also include validity properties.
+
+  -h, --help                  show this dialog
+      --debug                 enable additional logging output
+
+       -b, --batch                 process all data before returning final results as array
+  -d, --difficulty=<value>    override the minimum difficulty value
+  -e, --effort=<value>        increase demand on GPU processing
+  -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.
+Difficulty must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF.
+
+Report bugs: <bug-nano-pow@zoso.dev>
+Full documentation: <https://www.npmjs.com/package/nano-pow>
+`
+       )
+       process.exit(0)
 }
 
 const inArgs: string[] = []
@@ -37,10 +67,15 @@ while (/^[0-9A-Fa-f]{64}$/.test(args[args.length - 1] ?? '')) {
 }
 hashes.push(...inArgs)
 
-let fn = 'work_generate'
-let work = ''
-let isJson = false
-const options = {}
+if (hashes.length === 0) {
+       console.error('Invalid block hash input')
+       process.exit(1)
+}
+
+let isBatch = false
+const body: { [key: string]: any } = {
+       action: 'work_generate'
+}
 
 for (let i = 0; i < args.length; i++) {
        switch (args[i]) {
@@ -48,130 +83,70 @@ 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 = 'work_validate'
-                       work = `'${args[i + 1]}', `
+                       body.action = 'work_validate'
+                       body.work = args[i + 1]
                        break
                }
-               case ('--threshold'):
-               case ('-t'): {
-                       if (args[i + 1] == null) throw new Error('Missing argument for threshold')
-                       if (!/^[0-9A-Fa-f]{0,8}$/.test(args[i + 1])) throw new Error('Invalid threshold')
-                       options['threshold'] = parseInt(args[i + 1], 16)
+               case ('--difficulty'):
+               case ('-d'): {
+                       if (args[i + 1] == null) throw new Error('Missing argument for difficulty')
+                       if (!/^[0-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(args[i + 1])) throw new Error('Invalid difficulty')
+                       body.difficulty = args[i + 1]
                        break
                }
                case ('--effort'):
                case ('-e'): {
                        if (args[i + 1] == null) throw new Error('Missing argument for effort')
                        if (!/^[0-9]{0,2}$/.test(args[i + 1])) throw new Error('Invalid effort')
-                       options['effort'] = parseInt(args[i + 1], 10)
+                       process.env.NANO_POW_EFFORT = args[i + 1]
                        break
                }
-               case ('--debug'):
-               case ('-d'): {
-                       options['debug'] = true
+               case ('--debug'): {
+                       process.env.NANO_POW_DEBUG = 'true'
                        break
                }
-               case ('--json'):
-               case ('-j'): {
-                       isJson = true
+               case ('--batch'):
+               case ('-b'): {
+                       isBatch = true
                        break
                }
        }
 }
 
-if (options['debug']) {
-       console.log(`NanoPowCli.${fn}()`)
-       console.log(`${fn} options`, JSON.stringify(options))
-       for (const stdinErr of stdinErrors) {
-               console.warn(stdinErr)
-       }
+log('CLI args:', ...args)
+for (const stdinErr of stdinErrors) {
+       log(stdinErr)
 }
 
-if (hashes.length === 0) {
-       console.error('Invalid block hash input')
-       process.exit(1)
-}
+// Initialize server
+log('Starting NanoPow CLI')
+await import('./server.js')
 
-/**
-* Main
-*/
-(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,
-               args: [
-                       '--headless=new',
-                       '--use-angle=vulkan',
-                       '--enable-features=Vulkan',
-                       '--disable-vulkan-surface',
-                       '--enable-unsafe-webgpu'
-               ]
-       })
-       const page = await browser.newPage()
-       const path: string = new URL(import.meta.url).pathname
-       const dir = path.slice(0, path.lastIndexOf('/'))
-       await fs.writeFile(`${dir}/cli.html`, '')
-       await page.goto(import.meta.resolve('./cli.html'))
-       await page.waitForFunction(async (): Promise<GPUAdapter | null> => {
-               return await navigator.gpu.requestAdapter()
-       })
-
-       const inject = `
-               ${NanoPow}
-               window.results = []
-               const hashes = ["${hashes.join('","')}"]
-               for (const hash of hashes) {
-                       try {
-                               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}\`)
-                       }
-               }
-               console.log('cli exit')
-       `
-       const hash = await crypto.subtle.digest('SHA-256', Buffer.from(inject, 'utf-8'))
-       const src = `sha256-${Buffer.from(hash).toString('base64')}`
-
-       let start = performance.now()
-       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((): any => {
-                                               return (window as any).results
-                                       })
-                                       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) {
-                               try {
-                                       console.log(JSON.parse(output[1]))
-                               } catch (err) {
-                                       console.log(output[1])
-                               }
-                       }
-               } else if (options['debug']) {
-                       try {
-                               console.log(JSON.parse(msg.text()))
-                       } catch (err) {
-                               console.log(msg.text())
-                       }
+// Execution must be sequential else GPU cannot map to CPU and will throw
+const results: (WorkGenerateResponse | WorkValidateResponse)[] = []
+const start = performance.now()
+const aborter = new AbortController()
+for (const hash of hashes) {
+       try {
+               body.hash = hash
+               const kill = setTimeout(aborter.abort, 5000)
+               const response = await fetch(`http://localhost:${process.env.NANO_POW_PORT}`, {
+                       method: 'POST',
+                       body: JSON.stringify(body),
+                       signal: aborter.signal
+               })
+               clearTimeout(kill)
+               const result = await response.json()
+               if (isBatch) {
+                       results.push(result)
+               } else {
+                       console.log(result)
                }
-       })
-       start = performance.now()
-       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(`${dir}/cli.html`)
-       if (options['debug']) console.log('Puppeteer initialized')
-})()
+       } catch (err) {
+               log(err)
+       }
+}
+const end = performance.now()
+if (isBatch) console.log(results)
+log(end - start, 'ms total |', (end - start) / hashes.length, 'ms avg')
+process.exit(0)
index 9f4078f407719dd24a30b3391a23d60ed6b91eff..d999ce90fd468430336fe67c01c67a1d1e935f4b 100755 (executable)
 //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
-import * as crypto from 'node:crypto'
-import * as dns from 'node:dns/promises'
-import * as fs from 'node:fs/promises'
+import { readFile, unlink, writeFile } from 'node:fs/promises'
+import { launch, Browser, Page } from 'puppeteer'
+import { lookup } from 'node:dns/promises'
 import * as http from 'node:http'
-import * as os from 'node:os'
-import * as puppeteer from 'puppeteer'
-import { serverHelp } from '../../docs/index.js'
-import { NanoPowOptions, WorkGenerateRequest, WorkGenerateResponse, WorkValidateRequest, WorkValidateResponse } from '../types.js'
+import { hostname } from 'node:os'
+import { join } from 'node:path'
+import type { NanoPowOptions, WorkGenerateRequest, WorkGenerateResponse, WorkValidateRequest, WorkValidateResponse } from '#types'
 
-const PORT = process.env.PORT || 3000
-const EFFORT = +(process.env.NANO_POW_EFFORT || 8)
+process.title = 'NanoPow Server'
+const MAX_REQUEST_SIZE = 1024
+const MAX_BODY_SIZE = 158
 
-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')
+const DEBUG: boolean = !!(process.env.NANO_POW_DEBUG || false)
+const EFFORT: number = +(process.env.NANO_POW_EFFORT || 8)
+const PORT: number = +(process.env.NANO_POW_PORT || 3000)
 
-// Launch puppeteer browser instance - Persistent instance
-let browser: puppeteer.Browser
-let page: puppeteer.Page
+let browser: Browser
+let page: 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 (json: WorkGenerateRequest, options: NanoPowOptions): Promise<WorkGenerateResponse> => {
-                       if (json.difficulty) options.threshold = BigInt(`0x${json.difficulty}`)
-                       // @ts-expect-error
-                       return await window.NanoPow.work_generate(json.hash, options)
-               }, json, { debug: true, effort: EFFORT })
-               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' }))
-       }
+function log (...args: any[]): void {
+       if (DEBUG) console.log(new Date(Date.now()).toLocaleString(), 'NanoPow', args)
 }
 
-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
-       }
-
+async function respond (res: http.ServerResponse, data: Buffer[]): Promise<void> {
+       let statusCode: number = 500
+       let headers: http.OutgoingHttpHeaders = { 'Content-Type': 'application/json' }
+       let response: string = 'work_validate failed'
        try {
-               const result: WorkValidateResponse = await page.evaluate(async (json: WorkValidateRequest, options: NanoPowOptions): Promise<WorkValidateResponse> => {
-                       if (json.difficulty) options.threshold = BigInt(`0x${json.difficulty}`)
-                       //@ts-expect-error
-                       return await window.NanoPow.work_validate(json.work, json.hash, options)
-               }, json, { debug: true, effort: EFFORT })
-               res.writeHead(200, { 'Content-Type': 'application/json' })
-               res.end(JSON.stringify(result))
+               const datastring = Buffer.concat(data).toString().replace(/\s+/g, '')
+               if (Buffer.byteLength(datastring) > MAX_BODY_SIZE) {
+                       throw new Error('Invalid data.')
+               }
+               const { action, hash, work, difficulty }: WorkGenerateRequest | WorkValidateRequest = JSON.parse(datastring)
+               if (action !== 'work_generate' && action !== 'work_validate') {
+                       throw new Error('Invalid action. Must be work_generate or work_validate.')
+               }
+               if (!/^[0-9A-Fa-f]{64}$/.test(hash ?? '')) {
+                       throw new Error('Invalid hash. Must be a 64-character hex string.')
+               }
+               if (difficulty && !/^[1-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(difficulty)) {
+                       throw new Error('Invalid difficulty. Must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF.')
+               }
+               if (action === 'work_validate' && !/^[0-9A-Fa-f]{16}$/.test(work ?? '')) {
+                       throw new Error('Invalid work. Must be a 16-character hex string.')
+               }
+               const options: NanoPowOptions = {
+                       debug: DEBUG,
+                       effort: EFFORT,
+                       threshold: difficulty
+               }
+               const args = []
+               if (work) args.push(work)
+               args.push(hash)
+               args.push(options)
+               response = JSON.stringify(await page.evaluate(async (action: string, args: (string | NanoPowOptions)[]): Promise<WorkGenerateResponse | WorkValidateResponse> => {
+                       if (window.NanoPow == null) throw new Error('NanoPow not found')
+                       return await window.NanoPow[action](...args)
+               }, action, args))
+               statusCode = 200
        } catch (err) {
-               log('work_validate error:', err)
-               res.writeHead(500, { 'Content-Type': 'application/json' })
-               res.end(JSON.stringify({ error: 'work_validate failed' }))
+               log(err)
+               statusCode = 400
+       } finally {
+               res.writeHead(statusCode, headers).end(response)
        }
 }
 
-// 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())
-       })
-       const path: string = new URL(import.meta.url).pathname
-       const dir = path.slice(0, path.lastIndexOf('/'))
-       await fs.writeFile(`${dir}/server.html`, '')
-       await page.goto(import.meta.resolve('./server.html'))
-       await page.waitForFunction(async (): Promise<GPUAdapter | null> => {
-               return await navigator.gpu.requestAdapter()
-       })
+// Create server
+const server = http.createServer((req, res): void => {
+       let data: Buffer[] = []
+       let reqSize = 0
+       if (req.method === 'POST') {
+               req.on('data', (chunk: Buffer): void => {
+                       reqSize += chunk.byteLength
+                       if (reqSize > MAX_REQUEST_SIZE) {
+                               res.writeHead(413, { 'Content-Type': 'text/plain' })
+                               res.end('Content Too Large')
+                               req.socket.destroy()
+                               return
+                       }
+                       data.push(chunk)
+               })
+               req.on('end', async (): Promise<void> => {
+                       if (!req.socket.destroyed) {
+                               await respond(res, data)
+                       }
+               })
+       } else {
+               res.writeHead(200, { 'Content-Type': 'text/plain' })
+               res.end(`Usage: Send POST request to server URL to generate or validate Nano proof-of-work
 
-       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(`${dir}/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)
-               }
-       })
+Generate work for a BLOCKHASH with an optional DIFFICULTY:
+  curl -d '{ action: "work_generate", hash: BLOCKHASH, difficulty?: DIFFICULTY }'
 
-       server.on('error', (e): void => {
-               log('Server error', e)
-               try {
-                       shutdown()
-               } catch (err) {
-                       log('Failed to shut down', err)
-                       process.exit(1)
-               }
-       })
+Validate WORK previously calculated for a BLOCKHASH with an optional DIFFICULTY:
+  curl -d '{ action: "work_validate", work: WORK, hash: BLOCKHASH, difficulty?: DIFFICULTY }'
 
-       // Listen on configured port
-       server.listen(PORT, async (): Promise<void> => {
-               process.title = 'NanoPow Server'
-               const ip = await dns.lookup(os.hostname(), { family: 4 })
-               log(`Server process ${process.pid} running at ${ip.address}:${PORT}/`)
-       })
+BLOCKHASH is a 64-character hexadecimal string.
+WORK is 16-character hexadecimal string.
+DIFFICULTY is a 16-character hexadecimal string (default: FFFFFFF800000000)
 
-       // 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)
-               })
+Report bugs: <bug-nano-pow@zoso.dev>
+Full documentation: <https://www.npmjs.com/package/nano-pow>
+`
+               )
        }
-       process.on('SIGINT', shutdown)
-       process.on('SIGTERM', shutdown)
-})()
+})
+
+server.on('error', (e): void => {
+       log('Server error', e)
+       try {
+               shutdown()
+       } catch (err) {
+               log('Failed to shut down', err)
+               process.exit(1)
+       }
+})
+
+// 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((): Promise<never> => {
+               clearTimeout(kill)
+               log('Server stopped')
+               process.exit(0)
+       })
+}
+process.on('SIGINT', shutdown)
+process.on('SIGTERM', shutdown)
+
+// Initialize puppeteer
+log('Starting NanoPow work server')
+const NanoPow = await readFile(new URL('../main.min.js', import.meta.url), 'utf-8')
+browser = await 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 => log(msg.text()))
+
+const path: string = new URL(import.meta.url).pathname
+const dir = path.slice(0, path.lastIndexOf('/'))
+const filename = join(dir, `${process.pid}.html`)
+await writeFile(filename, '')
+await page.goto(import.meta.resolve(filename))
+await page.waitForFunction(async (): Promise<GPUAdapter | null> => {
+       return await navigator['gpu'].requestAdapter()
+})
+
+const src = `${NanoPow};window.NanoPow=NanoPow;`
+const hash = await crypto.subtle.digest('SHA-256', Buffer.from(src))
+const enc = `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 '${enc}';">
+                       <script type="module">${src}</script>
+               </head>
+       </html>
+`)
+await unlink(filename)
+log('Puppeteer initialized')
+
+// Listen on configured port
+server.listen(PORT, async (): Promise<void> => {
+       const ip = await lookup(hostname(), { family: 4 })
+       log(`Server process ${process.pid} running at ${ip.address}:${PORT}/`)
+})
index ad736e18b015fcc7b836dfe600f708ea753c1f25..0c90c2d5b5ec3833cefe6659eb800ce8a16186bc 100644 (file)
@@ -5,7 +5,7 @@
 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'
+import type { FBO, NanoPowOptions, WorkGenerateResponse, WorkValidateResponse } from '#types'
 
 /**
 * Nano proof-of-work using WebGL 2.0.
@@ -22,8 +22,6 @@ export class NanoPowGl {
        static #cores: number = Math.max(1, Math.floor(navigator.hardwareConcurrency))
        static #WORKLOAD: number = 256 * this.#cores
        static #canvas: OffscreenCanvas
-       /** 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
@@ -177,14 +175,14 @@ export class NanoPowGl {
 
                        /** Finalize configuration */
                        this.#query = this.#gl.createQuery()
-                       this.#pixels = new Uint32Array(this.size * 4)
+                       this.#pixels = new Uint32Array(this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight * 4)
                } catch (err) {
                        throw new Error('WebGL initialization failed.', { cause: err })
                } finally {
                        this.#busy = false
                }
                this.#isInitialized = true
-               console.log(`NanoPow WebGL initialized at ${this.#gl.drawingBufferWidth}x${this.#gl.drawingBufferHeight}. Maximum nonces checked per frame: ${this.size}`)
+               console.log(`NanoPow WebGL initialized. Maximum nonces checked per frame: ${this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight}`)
        }
 
        /**
index 08192831c1794e683a59f95e8a09c73a7ed4c995..d46eba032d422f0d7e1a98ec67c5cace93fc81cb 100644 (file)
@@ -1,9 +1,8 @@
 //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 //! SPDX-License-Identifier: GPL-3.0-or-later
-/// <reference types="@webgpu/types" />
 
 import { default as NanoPowGpuComputeShader } from './compute.wgsl'
-import type { NanoPowOptions, WorkGenerateRequest, WorkGenerateResponse, WorkValidateRequest, WorkValidateResponse } from '../../types.d.ts'
+import type { NanoPowOptions, WorkGenerateResponse, WorkValidateResponse } from '#types'
 
 /**
 * Nano proof-of-work using WebGPU.
index 1483ac10e471bcf41118313dd15d99ae56047479..2bffd006065476afeaa963fb9fabc4c7719a2791 100644 (file)
@@ -1,7 +1,11 @@
 // SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-import '@webgpu/types'
+declare global {
+       interface Window {
+               NanoPow: typeof NanoPow
+       }
+}
 
 /**
 * Used by work server for inbound requests to `work_generate`.
@@ -11,6 +15,7 @@ import '@webgpu/types'
 * @param {string} [difficulty=FFFFFFF800000000] - Minimum threshold for a nonce to be valid
 */
 type WorkGenerateRequest = {
+       [key: string]: string | undefined
        action: 'work_generate'
        hash: string
        difficulty?: string
@@ -24,6 +29,7 @@ type WorkGenerateRequest = {
 * @param {string} difficulty - BLAKE2b hash result which was compared to specified minimum threshold
 */
 type WorkGenerateResponse = {
+       [key: string]: string
        hash: string
        work: string
        difficulty: string
@@ -38,6 +44,7 @@ type WorkGenerateResponse = {
 * @param {string} [difficulty=FFFFFFF800000000] - Minimum threshold for a nonce to be valid
 */
 type WorkValidateRequest = {
+       [key: string]: string | undefined
        action: 'work_validate'
        hash: string
        work: string
@@ -55,6 +62,7 @@ type WorkValidateRequest = {
 * @param {string} valid_receive - 1 for true if nonce is valid for receive blocks, else 0 for false
 */
 type WorkValidateResponse = {
+       [key: string]: string | undefined
        hash: string
        work: string
        difficulty: string
@@ -103,9 +111,8 @@ export type NanoPowOptions = {
 * Nano proof-of-work using WebGL 2.0.
 */
 export declare class NanoPowGl {
+       static [key: string]: (...args: any[]) => any
        #private
-       /** Drawing buffer width in pixels. */
-       static get size (): number
        /**
        * Constructs canvas, gets WebGL context, initializes buffers, and compiles
        * shaders.
@@ -153,6 +160,7 @@ export declare class NanoPowGl {
 */
 export declare class NanoPowGpu {
        #private
+       static [key: string]: (...args: any[]) => any
        static init (): Promise<void>
        static setup (): void
        static reset (): void
index 2740258ef420de580e93e845cb3a0f2aaf42a072..301cf9de0547172b2e51248a87347ae684f69580 100644 (file)
@@ -8,4 +8,5 @@ BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E
 BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6
 32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F
 39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150
+badhash
 9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F
index d063e1f0945e314ec13871d29556375a931ffd37..83d05f74bdac1b7e95f438e501ca7d162c022b55 100644 (file)
@@ -210,7 +210,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
        <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;}
+               label.hex+input{padding-left:1.5em;}
        </style>
 </head>
 
index 6376fe9343931bee29b5f006759f50dc62aa1995..673ec119ed33e3062a079ef5b8da4807b155d283 100755 (executable)
@@ -1,20 +1,25 @@
 # SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-export NANO_POW_EFFORT=24
 SCRIPT_LINK=$(readlink -f "$0");
 SCRIPT_DIR=$(dirname "$SCRIPT_LINK");
 NANO_POW_HOME="$HOME"/.nano-pow;
 NANO_POW_LOGS="$NANO_POW_HOME"/logs;
 
-PORT=3001 "$SCRIPT_DIR"/../dist/bin/nano-pow.sh --server
+export NANO_POW_DEBUG=true
+export NANO_POW_EFFORT=24
+export NANO_POW_PORT=3001
+
+"$SCRIPT_DIR"/../dist/bin/nano-pow.sh --server
 sleep 2s
 
 printf '\nGet documentation\n'
 curl localhost:3001
 
-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:3001
+printf '\nExpect errors. Server should not crash when bad data is received like missing end quote or requests exceeding max size\n'
+curl -d '{ "action": "work_validate", "work": "47c83266398728cf", "hash: "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D" }' localhost:3001
+curl -d '{ "action": "work_validate", "work": "47c83266398728cf", "hash": "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D }' localhost:3001
+curl -d '{ "action": "work_validate", "work": "47c83266398728cf", "hash": "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D", "difficulty": "0000000000000000", "foo": "bar" }' localhost:3001
 
 printf '\nValidate good hashes\n'
 curl -d '{ "action": "work_validate", "work": "47c83266398728cf", "hash": "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D" }' localhost:3001
index 76109898e456f205f0675d8cbb64dd0f67de732c..edd4c53dd3405950a5595888504966e2242248fa 100644 (file)
                "noErrorTruncation": true,
                "noFallthroughCasesInSwitch": true,
                "strict": true,
-               "rootDir": "src"
+               "rootDir": "src",
+               "paths": {
+                       "#types": [
+                               "./src/types.d.ts"
+                       ]
+               },
+               "types": [
+                       "@webgpu/types"
+               ]
        },
        "include": [
                "src/main.ts",
-               "src/lib/*",
-               "src/lib/**/*"
+               "src/**/*.ts"
        ]
 }