+++ /dev/null
-//! 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>
-`
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'
})
#!/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[] = []
}
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]) {
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)
//! 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}/`)
+})
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.
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
/** 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}`)
}
/**
//! 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.
// 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`.
* @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
* @param {string} difficulty - BLAKE2b hash result which was compared to specified minimum threshold
*/
type WorkGenerateResponse = {
+ [key: string]: string
hash: string
work: string
difficulty: string
* @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
* @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
* 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.
*/
export declare class NanoPowGpu {
#private
+ static [key: string]: (...args: any[]) => any
static init (): Promise<void>
static setup (): void
static reset (): void
BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6
32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F
39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150
+badhash
9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F
<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>
# 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
"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"
]
}