From 4e31561b9e90d584ec8ef0b9d843a920b9fd9430 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Thu, 27 Mar 2025 14:03:44 -0700 Subject: [PATCH] Overhaul server to consolidate work methods into one function. Overhaul CLI to spin up server and request work instead of using its own puppeteer instance since testing found the server overhead to be negligible in comparison to puppeteer. Remove size getter from GL. Fix types and related imports. Add more environment variables to control server behavior. Add tests to server test script. Update package configuration files. --- docs/index.js | 38 ----- esbuild.mjs | 4 +- src/bin/cli.ts | 213 ++++++++++++--------------- src/bin/server.ts | 343 ++++++++++++++++++++----------------------- src/lib/gl/index.ts | 8 +- src/lib/gpu/index.ts | 3 +- src/types.d.ts | 14 +- test/blockhashes.txt | 1 + test/index.html | 2 +- test/script.sh | 13 +- tsconfig.json | 13 +- 11 files changed, 295 insertions(+), 357 deletions(-) delete mode 100644 docs/index.js diff --git a/docs/index.js b/docs/index.js deleted file mode 100644 index 5e10410..0000000 --- a/docs/index.js +++ /dev/null @@ -1,38 +0,0 @@ -//! 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 gather all results and output them at once 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 1-FFFFFFFFFFFFFFFF. - -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/esbuild.mjs b/esbuild.mjs index 14a0ba3..2d9ade2 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -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' }) diff --git a/src/bin/cli.ts b/src/bin/cli.ts index 5181b65..d1eb8e4 100755 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -1,34 +1,64 @@ #!/usr/bin/env node //! SPDX-FileCopyrightText: 2025 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -/// -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= override the minimum difficulty value + -e, --effort= increase demand on GPU processing + -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. +Difficulty must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF. + +Report bugs: +Full documentation: +` + ) + 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 => { - 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 => { - 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 => { - 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(` - - - - - - - `) - 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) diff --git a/src/bin/server.ts b/src/bin/server.ts index 9f4078f..d999ce9 100755 --- a/src/bin/server.ts +++ b/src/bin/server.ts @@ -2,201 +2,182 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! 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 { - 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 => { - 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 { - 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 { + 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 => { - 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 => { + 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 => { - // 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 => { - 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 => { + 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(` - - - - - - - `) - await fs.unlink(`${dir}/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) - } - }) +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 => { - 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 => { - await page?.close() - await browser?.close() - clearTimeout(kill) - log('Server stopped') - process.exit(0) - }) +Report bugs: +Full documentation: +` + ) } - 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 => { + 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 => { + 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(` + + + + + + +`) +await unlink(filename) +log('Puppeteer initialized') + +// Listen on configured port +server.listen(PORT, async (): Promise => { + const ip = await lookup(hostname(), { family: 4 }) + log(`Server process ${process.pid} running at ${ip.address}:${PORT}/`) +}) diff --git a/src/lib/gl/index.ts b/src/lib/gl/index.ts index ad736e1..0c90c2d 100644 --- a/src/lib/gl/index.ts +++ b/src/lib/gl/index.ts @@ -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}`) } /** diff --git a/src/lib/gpu/index.ts b/src/lib/gpu/index.ts index 0819283..d46eba0 100644 --- a/src/lib/gpu/index.ts +++ b/src/lib/gpu/index.ts @@ -1,9 +1,8 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -/// 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. diff --git a/src/types.d.ts b/src/types.d.ts index 1483ac1..2bffd00 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,7 +1,11 @@ // SPDX-FileCopyrightText: 2025 Chris Duncan // 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 static setup (): void static reset (): void diff --git a/test/blockhashes.txt b/test/blockhashes.txt index 2740258..301cf9d 100644 --- a/test/blockhashes.txt +++ b/test/blockhashes.txt @@ -8,4 +8,5 @@ BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6 32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F 39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150 +badhash 9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F diff --git a/test/index.html b/test/index.html index d063e1f..83d05f7 100644 --- a/test/index.html +++ b/test/index.html @@ -210,7 +210,7 @@ SPDX-License-Identifier: GPL-3.0-or-later diff --git a/test/script.sh b/test/script.sh index 6376fe9..673ec11 100755 --- a/test/script.sh +++ b/test/script.sh @@ -1,20 +1,25 @@ # SPDX-FileCopyrightText: 2025 Chris Duncan # 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 diff --git a/tsconfig.json b/tsconfig.json index 7610989..edd4c53 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,11 +13,18 @@ "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" ] } -- 2.34.1