})
await build({
- bundle: false,
+ bundle: true,
platform: 'node',
entryPoints: [
'./src/bin/cli.ts',
import { spawn } from 'node:child_process'
import { getRandomValues } from 'node:crypto'
import { createInterface } from 'node:readline/promises'
-import type { WorkGenerateResponse, WorkValidateResponse } from '#types'
+import { average, isHex, isNotHex, log } from '../utils'
+import { WorkGenerateResponse, WorkValidateResponse } from '#types'
process.title = 'NanoPow CLI'
delete process.env.NANO_POW_EFFORT
delete process.env.NANO_POW_PORT
-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[] = []
let i = 0
for await (const line of stdin) {
i++
- if (/^[0-9A-Fa-f]{64}$/.test(line)) {
+ if (isHex(line, 64)) {
hashes.push(line)
} else {
stdinErrors.push(`Skipping invalid stdin input line ${i}`)
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.
+Difficulty must be a hexadecimal string between 0-FFFFFFFFFFFFFFFF.
Report bugs: <bug-nano-pow@zoso.dev>
Full documentation: <https://www.npmjs.com/package/nano-pow>
}
const inArgs: string[] = []
-while (/^[0-9A-Fa-f]{64}$/.test(args[args.length - 1] ?? '')) {
+while (isHex(args[args.length - 1], 64)) {
inArgs.unshift(args.pop() as string)
}
hashes.push(...inArgs)
switch (args[i]) {
case ('--validate'):
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')
+ const v = args[i + 1]
+ if (v == null) throw new Error('Missing argument for work validation')
+ if (isNotHex(v, 16)) throw new Error('Invalid work to validate')
+ if (hashes.length !== 1) throw new Error('Validate accepts exactly one hash')
body.action = 'work_validate'
- body.work = args[i + 1]
+ body.work = v
break
}
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]
+ const d = args[i + 1]
+ if (d == null) throw new Error('Missing argument for difficulty')
+ if (isNotHex(d, 1, 16)) throw new Error('Invalid difficulty')
+ body.difficulty = d
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')
- process.env.NANO_POW_EFFORT = args[i + 1]
+ const e = args[i + 1]
+ if (e == null) throw new Error('Missing argument for effort')
+ if (parseInt(e) < 1 || parseInt(e) > 32) throw new Error('Invalid effort')
+ process.env.NANO_POW_EFFORT = e
break
}
case ('--benchmark'): {
- if (args[i + 1] == null) throw new Error('Missing argument for benchmark')
- if (!(+args[i + 1] > 0)) throw new Error('Invalid benchmark count')
+ const b = args[i + 1]
+ if (b == null) throw new Error('Missing argument for benchmark')
+ const count = +b
+ if (count < 1) throw new Error('Invalid benchmark count')
const random = new Uint8Array(32)
- while (hashes.length < +args[i + 1]) {
+ while (hashes.length < count) {
getRandomValues(random)
- const byteArray = [...random].map(byte => byte.toString(16).padStart(2, '0'))
- hashes.push(byteArray.join(''))
+ hashes.push(Buffer.from(random).toString('hex'))
}
isBenchmark = true
break
// Initialize server
log('Starting NanoPow CLI')
-const server = spawn(process.execPath, [new URL(import.meta.resolve('./server.js')).pathname], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] })
-const port = await new Promise((resolve, reject): void => {
- server.on('message', (msg: { type: string, port: number, text: string }): void => {
- if (msg.type === 'console') {
- log(msg.text)
- }
- if (msg.type === 'listening') {
- if (msg.port != null) {
- log(`Server listening on port ${msg.port}`)
- resolve(msg.port)
- } else {
- reject('Server failed to provide port')
- }
- }
- })
+
+const server = spawn(
+ process.execPath,
+ [new URL(import.meta.resolve('./server.js')).pathname],
+ { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] }
+)
+
+server.once('error', err => {
+ log(err)
+ process.exit(1)
})
-// Execution must be sequential else GPU cannot map to CPU and will throw
-const results: (WorkGenerateResponse | WorkValidateResponse)[] = []
-const aborter = new AbortController()
-if (isBenchmark) console.log('Starting benchmark...')
-const start = performance.now()
-for (const hash of hashes) {
- try {
- body.hash = hash
- const kill = setTimeout(() => aborter.abort(), 60000)
- const response = await fetch(`http://localhost:${port}`, {
- method: 'POST',
- body: JSON.stringify(body),
- signal: aborter.signal
- })
- clearTimeout(kill)
- const result = await response.json()
- if (isBatch || isBenchmark) {
- results.push(result)
+server.on('message', async (msg: { type: string, message: number | string }): Promise<void> => {
+ if (msg.type === 'console') {
+ log(msg.message)
+ }
+ if (msg.type === 'listening') {
+ const port = +msg.message
+ if (port > -1) {
+ log(`CLI server listening on port ${port}`)
+ try {
+ await execute(port)
+ } catch {
+ log(`Error executing ${body.action}`)
+ }
} else {
- console.log(result)
+ log('Server failed to provide port')
}
- } catch (err) {
- log(err)
}
-}
-const end = performance.now()
-if (isBatch && !isBenchmark) console.log(results)
-if (process.env.NANO_POW_DEBUG || isBenchmark) {
- console.log(end - start, 'ms total |', (end - start) / hashes.length, 'ms avg')
-}
+})
+
server.on('close', code => {
log(`Server closed with exit code ${code}`)
process.exit(code)
})
-server.kill()
+
+async function execute (port: number): Promise<void> {
+ // Execution must be sequential else GPU cannot map to CPU and will throw
+ const results: (WorkGenerateResponse | WorkValidateResponse)[] = []
+ if (isBenchmark) console.log('Running benchmark...')
+ let start = 0
+ const times: number[] = []
+ for (const hash of hashes) {
+ try {
+ const aborter = new AbortController()
+ const kill = setTimeout(() => aborter.abort(), 60_000)
+ body.hash = hash
+ start = performance.now()
+ const response = await fetch(`http://localhost:${port}`, {
+ method: 'POST',
+ body: JSON.stringify(body),
+ signal: aborter.signal
+ })
+ clearTimeout(kill)
+ const result = await response.json()
+ if (isBatch || isBenchmark) {
+ results.push(result)
+ times.push(performance.now() - start)
+ } else {
+ console.log(result)
+ }
+ } catch (err) {
+ log(err)
+ }
+ }
+ if (isBatch && !isBenchmark) console.log(results)
+ if (process.env.NANO_POW_DEBUG || isBenchmark) console.log(average(times))
+ server.kill()
+}
mkdir -p "$NANO_POW_LOGS";
if [ "$1" = '--server' ]; then
shift;
- node "$SCRIPT_DIR"/server.js --max-http-header-size=1024 >> "$NANO_POW_LOGS"/nano-pow-server-$(date -I).log 2>&1 & echo "$!" > "$NANO_POW_HOME"/server.pid;
+ node "$SCRIPT_DIR"/server.js --max-http-header-size=1024 --max-old-space-size=256 >> "$NANO_POW_LOGS"/nano-pow-server-$(date -I).log 2>&1 & echo "$!" > "$NANO_POW_HOME"/server.pid;
sleep 0.1;
if [ "$(ps | grep $(cat $NANO_POW_HOME/server.pid))" = '' ]; then
cat $(ls -td "$NANO_POW_LOGS"/* | head -n1);
fi;
else
- node "$SCRIPT_DIR"/cli.js "$@";
+ node "$SCRIPT_DIR"/cli.js --max-old-space-size=256 "$@";
fi;
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { launch } from 'puppeteer'
-import { subtle } from 'node:crypto'
-import { readFile } from 'node:fs/promises'
import * as http from 'node:http'
-import { AddressInfo, Socket } from 'node:net'
+import { hash } from 'node:crypto'
+import { readFile } from 'node:fs/promises'
+import { AddressInfo } from 'node:net'
import { homedir } from 'node:os'
import { join } from 'node:path'
-import type { NanoPowOptions, WorkGenerateRequest, WorkGenerateResponse, WorkValidateRequest, WorkValidateResponse } from '#types'
-/**
-* Override console logging to provide an informative prefix for each entry and
-* to only output when debug mode is enabled.
-*/
-function log (...args: any[]): void {
- if (CONFIG.DEBUG) {
- const text = `${new Date(Date.now()).toLocaleString(Intl.DateTimeFormat().resolvedOptions().locale ?? 'en-US', { hour12: false, dateStyle: 'medium', timeStyle: 'medium' })} NanoPow[${process.pid}]: ${args}`
- console.log(text)
- process.send?.({ type: 'console', text })
- }
-}
+import { launch } from 'puppeteer'
+
+import { isNotHex, log } from '../utils'
+import {
+ NanoPowOptions,
+ NanoPowServerConfig,
+ WorkGenerateRequest,
+ WorkGenerateResponse,
+ WorkValidateRequest,
+ WorkValidateResponse
+} from '#types'
process.title = 'NanoPow Server'
-const MAX_BODY_SIZE = 256
+
const MAX_CONNECTIONS = 1024
const MAX_HEADER_COUNT = 32
-const MAX_IDLE_TIME = 5000
+const MAX_IDLE_TIME = 5_000
const MAX_REQUEST_COUNT = 10
-const MAX_REQUEST_SIZE = 1024
-const MAX_REQUEST_TIME = 60000
+const MAX_REQUEST_SIZE = 256
+const MAX_REQUEST_TIME = 60_000
-const requests: Map<string, { tokens: number, time: number }> = new Map()
-setInterval(() => {
- for (const [i, t] of requests) {
- if (t.time < Date.now() - MAX_REQUEST_TIME) {
- requests.delete(i)
- }
- }
-}, Math.max(MAX_REQUEST_TIME, 0))
-
-const CONFIG = {
+const CONFIG: NanoPowServerConfig = {
DEBUG: false,
EFFORT: 8,
PORT: 5040
}
+const configPatterns = {
+ DEBUG: {
+ r: /^[ \t]*debug[ \t]*(true|false)[ \t]*(#.*)?$/i,
+ v: (b: string) => ['1', 'true', 'yes'].includes(b)
+ },
+ EFFORT: {
+ r: /^[ \t]*effort[ \t]*(\d{1,2})[ \t]*(#.*)?$/i,
+ v: (n: string) => +n
+ },
+ PORT: {
+ r: /^[ \t]*port[ \t]*(\d{1,5})[ \t]*(#.*)?$/i,
+ v: (n: string) => +n
+ }
+} as const
+
/**
* Loads the server configuration, preferring environment variables over the
* config file in the `.nano-pow` directory, and falling back to default values
* server is running will require a server restart to take effect.
*/
async function loadConfig (): Promise<void> {
- let contents = null
+ let configFile = ''
try {
- contents = await readFile(join(homedir(), '.nano-pow', 'config'), 'utf-8')
- } catch (err) {
+ configFile = await readFile(join(homedir(), '.nano-pow', 'config'), 'utf-8')
+ } catch {
log('Config file not found')
}
- if (typeof contents === 'string') {
- for (const line of contents.split('\n')) {
- const debugMatch = line.match(/^[ \t]*debug[ \t]*(true|false)[ \t]*(#.*)?$/i)
- if (Array.isArray(debugMatch)) {
- CONFIG.DEBUG = debugMatch[1] === 'true'
- }
- const effortMatch = line.match(/^[ \t]*effort[ \t]*(\d{1,2})[ \t]*(#.*)?$/i)
- if (Array.isArray(effortMatch)) {
- CONFIG.EFFORT = +effortMatch[1]
- }
-
- const portMatch = line.match(/^[ \t]*port[ \t]*(\d{1,5})[ \t]*(#.*)?$/i)
- if (Array.isArray(portMatch)) {
- CONFIG.PORT = +portMatch[1]
+ if (configFile.length > 0) {
+ for (const line of configFile.split('\n')) {
+ for (const [k, { r, v }] of Object.entries(configPatterns)) {
+ const m = r.exec(line)
+ if (m) CONFIG[k] = v(m[1])
}
}
}
- CONFIG.DEBUG = !!(process.env.NANO_POW_DEBUG) || CONFIG.DEBUG
+
+ CONFIG.DEBUG = ['1', 'true', 'yes'].includes(process.env.NANO_POW_DEBUG ?? '') || CONFIG.DEBUG
CONFIG.EFFORT = +(process.env.NANO_POW_EFFORT ?? '') || CONFIG.EFFORT
CONFIG.PORT = process.send ? 0 : +(process.env.NANO_POW_PORT ?? '') || CONFIG.PORT
+ log(`Config loaded ${JSON.stringify(CONFIG)}`)
}
+
await loadConfig()
-process.on('SIGHUP', async (): Promise<void> => {
- log('Reloading configuration')
- await loadConfig()
+
+// Initialize puppeteer
+log('Starting NanoPow work server')
+const NanoPow = await readFile(new URL('../main.min.js', import.meta.url), 'utf-8')
+const browser = await launch({
+ handleSIGHUP: false,
+ handleSIGINT: false,
+ handleSIGTERM: false,
+ headless: true,
+ args: [
+ '--headless=new',
+ '--disable-vulkan-surface',
+ '--enable-features=Vulkan,DefaultANGLEVulkan,VulkanFromANGLE',
+ '--enable-gpu',
+ '--enable-unsafe-webgpu'
+ ]
})
+const page = await browser.newPage()
-async function respond (res: http.ServerResponse, dataBuffer: Buffer[]): Promise<void> {
- let statusCode: number = 500
- let headers: http.OutgoingHttpHeaders = { 'Content-Type': 'application/json' }
- let response: string = 'request failed'
- try {
- const datastring = Buffer.concat(dataBuffer).toString()
- if (Buffer.byteLength(datastring) > MAX_BODY_SIZE) {
- throw new Error('Data too large.')
+const src = `${NanoPow};window.NanoPow=NanoPowGpu;`
+const enc = `sha256-${hash('sha256', src, 'base64')}`
+const body = `
+ <!DOCTYPE html>
+ <link rel="icon" href="data:,">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'; script-src '${enc}';">
+ <script type="module">${src}</script>
+`
+
+await page.setRequestInterception(true)
+page.on('request', req => {
+ if (req.isInterceptResolutionHandled()) return
+ if (req.url() === 'https://nanopow.invalid/') {
+ req.respond({ status: 200, contentType: 'text/html', body })
+ } else {
+ req.continue()
+ }
+})
+page.on('console', msg => log(msg.text()))
+await page.goto('https://nanopow.invalid/')
+
+let NanoPowHandle = await page.waitForFunction(() => {
+ return window.NanoPow
+})
+
+log('Puppeteer initialized')
+
+// Track requests by IP address, and let them fall off over time
+const requests = new Map<string, { time: number, tokens: number }>()
+
+setInterval(() => {
+ const now = Date.now()
+ for (const [ip, { time }] of requests) {
+ if (time < now - MAX_REQUEST_TIME) {
+ requests.delete(ip)
}
- const data: WorkGenerateRequest | WorkValidateRequest = JSON.parse(datastring)
- if (Object.getPrototypeOf(data) !== Object.prototype) {
+ }
+}, MAX_REQUEST_TIME)
+
+function get (res: http.ServerResponse): void {
+ res
+ .writeHead(200, { 'Content-Type': 'text/plain' })
+ .end(`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>
+`
+ )
+}
+
+async function post (res: http.ServerResponse, reqData: Buffer[]): Promise<void> {
+ const resHeaders: http.OutgoingHttpHeaders = { 'Content-Type': 'application/json' }
+ let resStatusCode = 500
+ let resBody = 'request failed'
+
+ try {
+ const reqBody: WorkGenerateRequest | WorkValidateRequest = JSON.parse(Buffer.concat(reqData).toString())
+ if (Object.getPrototypeOf(reqBody) !== Object.prototype) {
throw new Error('Data corrupted.')
}
- const { action, hash, work, difficulty } = data
+
+ const { action, hash, work, difficulty } = reqBody
if (action !== 'work_generate' && action !== 'work_validate') {
throw new Error('Action must be work_generate or work_validate.')
}
- response = `${action} failed`
- if (!/^[0-9A-Fa-f]{64}$/.test(hash ?? '')) {
+ resBody = `${action} failed`
+
+ if (isNotHex(hash, 64)) {
throw new Error('Hash must be a 64-character hex string.')
}
- if (difficulty && !/^[0-9A-Fa-f]{0,16}$/.test(difficulty)) {
+ if (difficulty && isNotHex(difficulty, 1, 16)) {
throw new Error('Difficulty must be a hex string between 0-FFFFFFFFFFFFFFFF.')
}
- if (action === 'work_validate' && !/^[0-9A-Fa-f]{16}$/.test(work ?? '')) {
+ if (action === 'work_validate' && isNotHex(work, 16)) {
throw new Error('Work must be a 16-character hex string.')
}
+
const options: NanoPowOptions = {
debug: CONFIG.DEBUG,
effort: CONFIG.EFFORT,
difficulty
}
- switch (action) {
- case ('work_generate'): {
- response = JSON.stringify(await page.evaluate(async (np, hash: string, options: NanoPowOptions): Promise<WorkGenerateResponse | WorkValidateResponse> => {
- if (np == null) throw new Error('NanoPow not found')
- return await np.work_generate(hash, options)
- }, npHandle, hash, options))
- break
- }
- case ('work_validate'): {
- response = JSON.stringify(await page.evaluate(async (np, work: string, hash: string, options: NanoPowOptions): Promise<WorkGenerateResponse | WorkValidateResponse> => {
- if (np == null) throw new Error('NanoPow not found')
- return await np.work_validate(work, hash, options)
- }, npHandle, work, hash, options))
- break
- }
- default: {
- throw new Error('Action must be work_generate or work_validate.')
- }
- }
- statusCode = 200
+ const result = (action === 'work_generate')
+ ? await page.evaluate((n, h, o): Promise<WorkGenerateResponse> => {
+ if (n == null) throw new Error('NanoPow not found')
+ return n.work_generate(h, o)
+ }, NanoPowHandle, hash, options)
+ : await page.evaluate((n, w, h, o): Promise<WorkValidateResponse> => {
+ if (n == null) throw new Error('NanoPow not found')
+ return n.work_validate(w, h, o)
+ }, NanoPowHandle, work, hash, options)
+ resBody = JSON.stringify(result)
+ resStatusCode = 200
} catch (err) {
log(err)
- statusCode = 400
+ resStatusCode = 400
} finally {
- res.writeHead(statusCode, headers).end(response)
+ res.writeHead(resStatusCode, resHeaders).end(resBody)
}
}
-// Create server
-const server = http.createServer((req, res): void => {
+/**
+* Parses headers to retrieve true client IP address.
+*
+* @param {IncomingMessage} req - Client request to parse
+* @returns IP address from headers or socket or undefined
+*/
+function getIp (req: http.IncomingMessage): string | undefined {
const xff = req.headers['x-forwarded-for']
const ip = (typeof xff === 'string')
? xff.split(',')[0].trim().replace(/^::ffff:/, '')
: req.socket.remoteAddress
- if (ip == null) {
- res.writeHead(401).end('Unauthorized')
- return
+ return ip
+}
+
+/**
+* Checks if IP address has requested too much work recently. Ignores IPC and
+* local clients which are used by the CLI.
+*
+* @param ip - Client IP address, possibly local
+* @returns True if IP is out of time-limited tokens and should be throttled
+*/
+function isRateLimited (ip: string): boolean {
+ if (ip === '127.0.0.1' || ip === '::1' || process.send) {
+ return false
}
const client = requests.get(ip)
- if (ip === '127.0.0.1' || process.send != null || client == null || client.time < Date.now() - MAX_REQUEST_TIME) {
+ if (client && client.tokens-- <= 0) {
+ log(`==== Potential Abuse: ${ip} ====`)
+ return true
+ }
+ if (Date.now() - MAX_REQUEST_TIME > (client?.time ?? 0)) {
requests.set(ip, { tokens: MAX_REQUEST_COUNT, time: Date.now() })
- } else {
- if (--client.tokens <= 0) {
- log(`${ip} potential abuse`)
- res.writeHead(429).end('Too Many Requests')
- return
- }
}
- let data: Buffer[] = []
- let reqSize = 0
- if (req.method === 'POST') {
+ return false
+}
+
+/**
+* Starts server listening on configured port. If spawned by IPC, sends a message
+* to the parent process with the port number assigned by the operating system.
+*/
+function listen (): void {
+ server.listen(CONFIG.PORT, '127.0.0.1', () => {
+ const { port } = server.address() as AddressInfo
+ CONFIG.PORT = port
+ log(`Server listening on port ${port}`)
+ process.send?.({ type: 'listening', message: port })
+ })
+}
+
+/**
+* Reads streaming data from an inbound client request.
+*
+* @param {IncomingMessage} req - Client request for which to process data
+* @returns Buffer array of final data
+*/
+async function readIncomingMessage (req: http.IncomingMessage): Promise<Buffer[]> {
+ return new Promise((resolve, reject) => {
const contentLength = +(req.headers['content-length'] ?? 0)
- if (contentLength == 0 || contentLength > MAX_BODY_SIZE) {
- res.writeHead(413).end('Content Too Large')
- req.socket.destroy()
- return
+ if (contentLength === 0 || contentLength > MAX_REQUEST_SIZE) {
+ reject(new Error('Content Too Large', { cause: { code: 413 } }))
}
- req.on('data', (chunk: Buffer): void => {
+ const kill = setTimeout(() => {
+ reject(new Error('Request Timeout', { cause: { code: 408 } }))
+ }, MAX_IDLE_TIME)
+ const reqData: Buffer[] = []
+ let reqSize = 0
+ req.on('data', chunk => {
reqSize += chunk.byteLength
if (reqSize > MAX_REQUEST_SIZE) {
- res.writeHead(413).end('Content Too Large')
- req.socket.destroy()
- return
+ reject(new Error('Content Too Large', { cause: { code: 413 } }))
}
- data.push(chunk)
+ reqData.push(chunk)
})
- req.on('end', async (): Promise<void> => {
- if (!req.socket.destroyed) {
- await respond(res, data)
- }
+ req.on('end', () => {
+ clearTimeout(kill)
+ resolve(reqData)
})
- } 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
-
-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)
+ req.once('error', reject)
+ })
+}
-Report bugs: <bug-nano-pow@zoso.dev>
-Full documentation: <https://www.npmjs.com/package/nano-pow>
-`
- )
+/**
+* Creates server and configures POST data aggregation and GET response call.
+*/
+const server = http.createServer(async (req, res) => {
+ const ip = getIp(req)
+ if (!ip) return res.writeHead(401).end('Unauthorized')
+ if (isRateLimited(ip)) return res.writeHead(429).end('Too Many Requests')
+ if (req.method === 'POST') {
+ try {
+ const reqData = await readIncomingMessage(req)
+ post(res, reqData)
+ } catch (err: any) {
+ req.socket.destroy()
+ return res.writeHead(err.cause?.code ?? 500).end(err.message ?? 'Internal Server Error')
+ }
+ } else {
+ get(res)
}
})
server.maxConnections = MAX_CONNECTIONS
server.maxHeadersCount = MAX_HEADER_COUNT
-server.on('connection', (c: Socket): void => {
- c.setTimeout(MAX_IDLE_TIME, () => c.destroy())
+server.on('connection', s => {
+ s.setTimeout(MAX_IDLE_TIME, () => s.destroy())
})
-server.on('error', (e: Error): void => {
- log('Server error', e)
+server.on('error', serverErr => {
+ log('Server error', serverErr)
try {
shutdown()
- } catch (err) {
- log('Failed to shut down', err)
+ } catch (shutdownErr) {
+ log('Failed to shut down', shutdownErr)
process.exit(1)
}
})
-// Shut down server gracefully when process is terminated
+/**
+* Shuts down server gracefully when process is terminated or forcefully if it
+* does not respond within 10 seconds.
+*/
function shutdown (): void {
log('Shutdown signal received')
const kill = setTimeout((): never => {
log('Server unresponsive, forcefully stopped')
process.exit(1)
- }, 10000)
+ }, 10_000)
server.close(async (): Promise<never> => {
await browser.close()
clearTimeout(kill)
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')
-const browser = await launch({
- handleSIGHUP: false,
- handleSIGINT: false,
- handleSIGTERM: false,
- headless: true,
- args: [
- '--headless=new',
- '--disable-vulkan-surface',
- '--enable-features=Vulkan,DefaultANGLEVulkan,VulkanFromANGLE',
- '--enable-gpu',
- '--enable-unsafe-webgpu'
- ]
-})
-const page = await browser.newPage()
-
-const src = `${NanoPow};window.NanoPow=NanoPowGpu;`
-const hash = await subtle.digest('SHA-256', Buffer.from(src))
-const enc = `sha256-${Buffer.from(hash).toString('base64')}`
-const body = `
- <!DOCTYPE html>
- <link rel="icon" href="data:,">
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'; script-src '${enc}';">
- <script type="module">${src}</script>
-`
-
-await page.setRequestInterception(true)
-page.on('request', async (req): Promise<void> => {
- if (req.isInterceptResolutionHandled()) return
- if (req.url() === 'https://nanopow.invalid/') {
- req.respond({ status: 200, contentType: 'text/html', body })
- } else {
- req.continue()
- }
-})
-page.on('console', msg => log(msg.text()))
-await page.goto('https://nanopow.invalid/')
-await page.waitForFunction(async (): Promise<boolean> => {
- return window.NanoPow != null
+process.on('SIGHUP', async () => {
+ log('Reloading configuration')
+ server.close(async () => {
+ await loadConfig()
+ await page.reload()
+ NanoPowHandle = await page.waitForFunction(() => {
+ return window.NanoPow
+ })
+ listen()
+ })
})
-const npHandle = await page.evaluateHandle(() => window.NanoPow)
-log('Puppeteer initialized')
-
-// Listen on configured port
-server.listen(CONFIG.PORT, '127.0.0.1', async (): Promise<void> => {
- const { port } = server.address() as AddressInfo
- CONFIG.PORT = port
- log(`Server listening on port ${port}`)
- process.send?.({ type: 'listening', port })
-})
+listen()
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 { isNotHex } from '#utils'
import type { FBO, NanoPowOptions, WorkGenerateResponse, WorkValidateResponse } from '#types'
/**
/** Finalize configuration */
this.#query = this.#gl.createQuery()
this.#pixels = new Uint32Array(this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight * 4)
+ console.log(`NanoPow WebGL initialized. Maximum nonces checked per frame: ${this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight}`)
+ this.#isInitialized = true
} catch (err) {
throw new Error('WebGL initialization failed.', { cause: err })
} finally {
this.#busy = false
}
- this.#isInitialized = true
- console.log(`NanoPow WebGL initialized. Maximum nonces checked per frame: ${this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight}`)
}
/**
* @param {NanoPowOptions} options - Options used to configure search execution
*/
static async work_generate (hash: string, options?: NanoPowOptions): Promise<WorkGenerateResponse> {
- if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`)
+ if (isNotHex(hash, 64)) throw new Error(`Invalid hash ${hash}`)
if (this.#busy) {
console.log('NanoPowGl is busy. Retrying search...')
return new Promise(resolve => {
})
}
if (this.#isInitialized === false) this.init()
- this.#busy = true
- if (typeof options?.difficulty === 'string') {
- try {
- options.difficulty = BigInt(`0x${options.difficulty}`)
- } catch (err) {
- throw new TypeError(`Invalid difficulty ${options.difficulty}`)
- }
+ options ??= {}
+ options.debug ??= false
+ options.difficulty ??= 0xfffffff800000000n
+ options.effort ??= 0x4
+
+ if (typeof options?.difficulty !== 'string'
+ && typeof options?.difficulty !== 'bigint'
+ ) {
+ throw new TypeError(`Invalid difficulty ${options?.difficulty}`)
}
- const difficulty = (typeof options?.difficulty !== 'bigint' || options.difficulty < 1n || options.difficulty > 0xffffffffffffffffn)
- ? 0xfffffff800000000n
+ const difficulty = typeof options.difficulty === 'string'
+ ? BigInt(`0x${options.difficulty}`)
: options.difficulty
+
+ if (difficulty < 0x0n || difficulty > 0xfffffff800000000n) {
+ throw new TypeError(`Invalid difficulty ${options.difficulty}`)
+ }
+
const effort = (typeof options?.effort !== 'number' || options.effort < 0x1 || options.effort > 0x20)
? this.#cores
: options.effort
+
+ this.#busy = true
+
this.#debug = !!(options?.debug)
+
if (this.#debug) console.log('NanoPowGl.work_generate()')
if (this.#debug) console.log('blockhash', hash)
if (this.#debug) console.log('search options', JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
* @param {NanoPowOptions} options - Options used to configure search execution
*/
static async work_validate (work: string, hash: string, options?: NanoPowOptions): Promise<WorkValidateResponse> {
- if (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new Error(`Invalid work ${work}`)
- if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`)
+ if (isNotHex(work, 16)) throw new Error(`Invalid work ${work}`)
+ if (isNotHex(hash, 64)) throw new Error(`Invalid hash ${hash}`)
if (this.#busy) {
console.log('NanoPowGl is busy. Retrying validate...')
return new Promise(resolve => {
})
}
if (this.#isInitialized === false) this.init()
- this.#busy = true
- if (typeof options?.difficulty === 'string') {
- try {
- options.difficulty = BigInt(`0x${options.difficulty}`)
- } catch (err) {
- throw new TypeError(`Invalid difficulty ${options.difficulty}`)
- }
+ options ??= {}
+ options.debug ??= false
+ options.difficulty ??= 0xfffffff800000000n
+ options.effort ??= 0x4
+
+ if (typeof options?.difficulty !== 'string'
+ && typeof options?.difficulty !== 'bigint'
+ ) {
+ throw new TypeError(`Invalid difficulty ${options?.difficulty}`)
}
- const difficulty = (typeof options?.difficulty !== 'bigint' || options.difficulty < 1n || options.difficulty > 0xffffffffffffffffn)
- ? 0xfffffff800000000n
+ const difficulty = typeof options.difficulty === 'string'
+ ? BigInt(`0x${options.difficulty}`)
: options.difficulty
+
+ if (difficulty < 0x0n || difficulty > 0xfffffff800000000n) {
+ throw new TypeError(`Invalid difficulty ${options.difficulty}`)
+ }
+
+ this.#busy = true
+
this.#debug = !!(options?.debug)
+
if (this.#debug) console.log('NanoPowGl.work_validate()')
if (this.#debug) console.log('blockhash', hash)
if (this.#debug) console.log('validate options', JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
*/
struct UBO {
blockhash: array<vec4<u32>, 2>,
- seed: vec2<u32>,
- difficulty: vec2<u32>
+ difficulty: vec2<u32>,
+ validate: u32,
+ seed: vec2<u32>
};
@group(0) @binding(0) var<uniform> ubo: UBO;
*/
const Z = vec2(0u);
+/**
+* Shared flag to prevent execution for all workgroup threads based on the
+* atomicLoad() result of a single member thread.
+*/
+var<workgroup> validate: bool;
+
/**
* Shared flag to prevent execution for all workgroup threads based on the
* atomicLoad() result of a single member thread.
var<workgroup> m4: vec2<u32>;
/**
-* Search compute function
-* Calls main with a workgroup size of 64 which balances warps between NVIDIA and
+* Main compute function
+*
+* Computes with a workgroup size of 64 which balances warps between NVIDIA and
* AMD cards while still considering the power-sensitive requirements of mobile
* devices. The entire workgroup exits immediately if a nonce was already found
* by a previous workgroup.
+*
+* Each component of a random 8-byte value, provided by the UBO as a vec2<u32>,
+* is XOR'd with a different dimensional index from the global thread identifier
+* to create a unique nonce value for each thread.
+*
+* Where the reference implementation uses array lookups, the NanoPow
+* implementation assigns each array element to its own variable to enhance
+* performance, but the variable name still contains the original index digit.
*/
@compute @workgroup_size(64)
-fn search(@builtin(global_invocation_id) global_id: vec3<u32>, @builtin(local_invocation_id) local_id: vec3<u32>) {
+fn main(@builtin(global_invocation_id) global_id: vec3<u32>, @builtin(local_invocation_id) local_id: vec3<u32>) {
if (local_id.x == 0u) {
+ validate = ubo.validate == 1u;
found = atomicLoad(&work.found) != 0u;
seed = ubo.seed;
m1 = ubo.blockhash[0u].xy;
}
workgroupBarrier();
if (found) { return; }
- main(global_id, false);
-}
-
-/**
-* Validate compute function
-* Calls main with a workgroup size of 1 so that only one value is tested.
-*/
-@compute @workgroup_size(1)
-fn validate(@builtin(global_invocation_id) global_id: vec3<u32>) {
- seed = ubo.seed;
- m1 = ubo.blockhash[0u].xy;
- m2 = ubo.blockhash[0u].zw;
- m3 = ubo.blockhash[1u].xy;
- m4 = ubo.blockhash[1u].zw;
- main(global_id, true);
-}
-/**
-* Main compute function
-* Each component of a random 8-byte value, provided by the UBO as a vec2<u32>,
-* is XOR'd with a different dimensional index from the global thread identifier
-* to create a unique nonce value for each thread.
-*
-* Where the reference implementation uses array lookups, the NanoPow
-* implementation assigns each array element to its own variable to enhance
-* performance, but the variable name still contains the original index digit.
-*/
-fn main(id: vec3<u32>, validate: bool) {
/**
* Initialize unique nonce
*/
- let m0: vec2<u32> = seed ^ id.xy;
+ let m0: vec2<u32> = seed ^ global_id.xy;
/**
* Compression buffer copied from the modified initialization vector.
* Set nonce if it passes the difficulty threshold and no other thread has set it.
*/
var result: vec2<u32> = BLAKE2B_INIT[0u] ^ v0 ^ v8;
- if (validate || ((result.y > ubo.difficulty.y || (result.y == ubo.difficulty.y && result.x >= ubo.difficulty.x)) && atomicLoad(&work.found) == 0u)) {
- atomicStore(&work.found, 1u);
- work.nonce = m0;
- work.result = result;
+ if (select((result.y > ubo.difficulty.y || (result.y == ubo.difficulty.y && result.x >= ubo.difficulty.x)), all(global_id == vec3(0u)), validate)) {
+ loop {
+ let swap = atomicCompareExchangeWeak(&work.found, 0u, 1u);
+ if (swap.exchanged) {
+ work.nonce = m0;
+ work.result = result;
+ break;
+ }
+ if (swap.old_value != 0u) {
+ break;
+ }
+ }
}
return;
}
//! SPDX-License-Identifier: GPL-3.0-or-later
import { default as NanoPowGpuComputeShader } from './compute.wgsl'
-import type { NanoPowOptions, WorkGenerateResponse, WorkValidateResponse } from '#types'
+import { isNotHex } from '#utils'
+import type { NanoPowOptions, NanoPowResult, WorkGenerateResponse, WorkValidateResponse } from '#types'
/**
* Nano proof-of-work using WebGPU.
// Initialize WebGPU
static #isInitialized: boolean = false
- static #busy: boolean = false
static #debug: boolean = false
+ static #action: 'work_generate' | 'work_validate' = 'work_generate'
+ static #difficulty: bigint = this.#SEND
+ static #effort: number = 4
+ static #queue: any[] = []
static #device: GPUDevice | null = null
- static #bufferReset: BigUint64Array = new BigUint64Array([0n, 0n, 0n, 0n])
+ static #bufferReset: BigUint64Array = new BigUint64Array(4)
+ static #uboArray: BigUint64Array = new BigUint64Array(8)
+ static #uboView: DataView
+ static #uboBuffer: GPUBuffer
static #gpuBuffer: GPUBuffer
static #cpuBuffer: GPUBuffer
- static #uboBuffer: GPUBuffer
- static #uboView: DataView
+ static #bindGroupLayout: GPUBindGroupLayout | null
+ static #bindGroup: GPUBindGroup | null
+ static #pipeline: GPUComputePipeline | null
static #resultView: DataView
- static #bindGroupLayout: GPUBindGroupLayout
- static #bindGroup: GPUBindGroup
- static #searchPipeline: GPUComputePipeline
- static #validatePipeline: GPUComputePipeline
+ static #result: NanoPowResult
// Initialize WebGPU
static async init (): Promise<void> {
- if (this.#busy) return
- this.#busy = true
+ console.log('Initializing NanoPowGpu.')
// Request device and adapter
try {
if (navigator.gpu == null) throw new Error('WebGPU is not supported in this browser.')
if (!(device instanceof GPUDevice)) throw new Error('WebGPU device failed to load.')
device.lost?.then(this.reset)
this.#device = device
- await this.setup()
+
+ // Create buffers for writing GPU calculations and reading from Javascript
+ this.#uboView = new DataView(this.#uboArray.buffer)
+ this.#uboBuffer = this.#device.createBuffer({
+ label: 'ubo',
+ size: 64,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
+ })
+ this.#gpuBuffer = this.#device.createBuffer({
+ label: 'gpu',
+ size: 32,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
+ })
+ this.#cpuBuffer = this.#device.createBuffer({
+ label: 'cpu',
+ size: 32,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
+ })
+
+ // Create binding group data structure and use it later once UBO is known
+ this.#bindGroupLayout = this.#device.createBindGroupLayout({
+ entries: [
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' }, },
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' }, },
+ ],
+ })
+ // Create pipeline to connect compute shader to binding layout
+ this.#pipeline = this.#device.createComputePipeline({
+ layout: this.#device.createPipelineLayout({
+ bindGroupLayouts: [this.#bindGroupLayout]
+ }),
+ compute: {
+ entryPoint: 'main',
+ module: this.#device.createShaderModule({
+ code: NanoPowGpuComputeShader
+ })
+ }
+ })
+ // Bind UBO read and GPU write buffers
+ this.#bindGroup = this.#device.createBindGroup({
+ layout: this.#bindGroupLayout,
+ entries: [
+ { binding: 0, resource: { buffer: this.#uboBuffer }, },
+ { binding: 1, resource: { buffer: this.#gpuBuffer }, },
+ ],
+ })
+ // Compile and cache shader prior to actual dispatch
+ const cmd = this.#device.createCommandEncoder()
+ cmd.beginComputePass().end()
+ this.#device.queue.submit([cmd.finish()])
+ await this.#device.queue.onSubmittedWorkDone()
+ console.log(`NanoPow WebGPU initialized.`)
+ this.#isInitialized = true
} catch (err) {
throw new Error('WebGPU initialization failed.', { cause: err })
- } finally {
- this.#busy = false
}
- this.#isInitialized = true
- }
-
- static async setup (): Promise<void> {
- if (this.#device == null) throw new Error(`WebGPU device failed to load.`)
- // Create buffers for writing GPU calculations and reading from Javascript
- this.#gpuBuffer = this.#device.createBuffer({
- size: 32,
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
- })
- this.#cpuBuffer = this.#device.createBuffer({
- size: 32,
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
- })
- this.#uboBuffer = this.#device.createBuffer({
- size: 48,
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
- })
- this.#uboView = new DataView(new ArrayBuffer(48))
- // Create binding group data structure and use it later once UBO is known
- this.#bindGroupLayout = this.#device.createBindGroupLayout({
- entries: [
- {
- binding: 0,
- visibility: GPUShaderStage.COMPUTE,
- buffer: { type: 'uniform' },
- },
- {
- binding: 1,
- visibility: GPUShaderStage.COMPUTE,
- buffer: { type: 'storage' },
- }
- ]
- })
- const shaderModule = this.#device.createShaderModule({
- code: NanoPowGpuComputeShader
- })
- // Create pipeline to connect compute shader to binding layout
- this.#searchPipeline = this.#device.createComputePipeline({
- layout: this.#device.createPipelineLayout({
- bindGroupLayouts: [this.#bindGroupLayout]
- }),
- compute: {
- entryPoint: 'search',
- module: shaderModule
- }
- })
- // Bind UBO read and GPU write buffers
- this.#bindGroup = this.#device.createBindGroup({
- layout: this.#bindGroupLayout,
- entries: [
- {
- binding: 0,
- resource: {
- buffer: this.#uboBuffer
- },
- },
- {
- binding: 1,
- resource: {
- buffer: this.#gpuBuffer
- },
- },
- ],
- })
- // Create pipeline to connect compute shader to binding layout
- this.#validatePipeline = this.#device.createComputePipeline({
- layout: this.#device.createPipelineLayout({
- bindGroupLayouts: [this.#bindGroupLayout]
- }),
- compute: {
- entryPoint: 'validate',
- module: shaderModule
- }
- })
- // Compile and cache shader prior to actual dispatch
- const cmd = this.#device.createCommandEncoder()
- cmd.beginComputePass().end()
- this.#device.queue.submit([cmd.finish()])
- await this.#device.queue.onSubmittedWorkDone()
- console.log(`NanoPow WebGPU initialized. Recommended effort: ${Math.max(1, Math.floor(navigator.hardwareConcurrency / 2))}`)
}
static reset (): void {
NanoPowGpu.#cpuBuffer?.destroy()
NanoPowGpu.#gpuBuffer?.destroy()
NanoPowGpu.#uboBuffer?.destroy()
- NanoPowGpu.#busy = false
+ NanoPowGpu.#bindGroupLayout = null
+ NanoPowGpu.#bindGroup = null
+ NanoPowGpu.#pipeline = null
NanoPowGpu.#isInitialized = false
- NanoPowGpu.init()
+ queueMicrotask(() => NanoPowGpu.init().catch(console.log))
}
- static #logAverages (times: number[]): void {
- let count = times.length, truncatedCount = 0, truncated = 0, sum = 0, reciprocals = 0, logarithms = 0, min = Number.MAX_SAFE_INTEGER, max = 0, median = 0, rate = 0
- times.sort()
- for (let i = 0; i < count; i++) {
- sum += times[i]
- reciprocals += 1 / times[i]
- logarithms += Math.log(times[i])
- min = Math.min(min, times[i])
- max = Math.max(max, times[i])
- if (i === Math.ceil(count / 2)) {
- median = times[i]
- }
- if (count < 3 || (i > (0.1 * count) && i < (0.9 * (count - 1)))) {
- truncated += times[i]
- truncatedCount++
- }
+ /**
+ * Validate work, if present, and blockhash.
+ * Validate options and normalize its properties.
+ */
+ static async #work_init (work: string | null, hash: string, options?: NanoPowOptions): Promise<void> {
+ if (this.#isInitialized === false) await this.init()
+ if (this.#debug) console.log(this.#action)
+ if (isNotHex(hash, 64)) throw new TypeError(`Invalid hash ${hash}`)
+ if (work != null && isNotHex(work, 16)) throw new TypeError(`Invalid work ${work}`)
+ options ??= {}
+ options.debug ??= false
+ options.difficulty ??= this.#SEND
+ options.effort ??= 0x4
+
+ if (typeof options.effort !== 'number'
+ || options.effort < 0x1
+ || options.effort > 0x20
+ ) {
+ throw new TypeError(`Invalid effort ${options.effort}`)
}
- const averages = {
- "Count (dispatches)": count,
- "Total (ms)": sum,
- "Rate (d/s)": 1000 * truncatedCount / (truncated || sum),
- "Minimum (ms)": min,
- "Maximum (ms)": max,
- "Median (ms)": median,
- "Arithmetic Mean (ms)": sum / count,
- "Truncated Mean (ms)": truncated / truncatedCount,
- "Harmonic Mean (ms)": count / reciprocals,
- "Geometric Mean (ms)": Math.exp(logarithms / count)
+ this.#effort = this.#action === 'work_generate'
+ ? options.effort * 0x100
+ : 1
+
+ if (typeof options.difficulty !== 'string'
+ && typeof options.difficulty !== 'bigint'
+ ) {
+ throw new TypeError(`Invalid difficulty ${options.difficulty}`)
+ }
+ this.#difficulty = typeof options.difficulty === 'string'
+ ? BigInt(`0x${options.difficulty}`)
+ : options.difficulty
+ if (this.#difficulty < 0x0n || this.#difficulty > this.#SEND) {
+ throw new TypeError(`Invalid difficulty ${options.difficulty}`)
}
- console.table(averages)
- }
- static async #dispatch (pipeline: GPUComputePipeline, seed: bigint, hash: string, difficulty: bigint, passes: number): Promise<void> {
- if (this.#device == null) throw new Error(`WebGPU device failed to load.`)
- // Set up uniform buffer object
- // Note: u32 size is 4, but total alignment must be multiple of 16
- for (let i = 0; i < this.#uboView.byteLength; i++) this.#uboView.setUint8(i, 0)
- for (let i = 0; i < 64; i += 16) {
- const u64 = hash.slice(i, i + 16)
- this.#uboView.setBigUint64(i / 2, BigInt(`0x${u64}`))
+ this.#debug = !!options.debug
+ if (this.#debug) {
+ if (work) console.log('work', work)
+ console.log('blockhash', hash)
+ console.log(`options`, JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
}
- this.#uboView.setBigUint64(32, seed, true)
- this.#uboView.setBigUint64(40, difficulty, true)
- if (this.#debug) console.log('UBO', this.#uboView)
- this.#device.queue.writeBuffer(this.#uboBuffer, 0, this.#uboView)
+ // Ensure WebGPU is initialized before calculating
+ let loads = 0
+ while (this.#device == null && loads++ < 20) {
+ await new Promise(resolve => {
+ setTimeout(resolve, 100)
+ })
+ }
+ if (this.#device == null) {
+ throw new Error(`WebGPU device failed to load.`)
+ }
// Reset WORK properties to 0u before each calculation
this.#device.queue.writeBuffer(this.#gpuBuffer, 0, this.#bufferReset)
this.#device.queue.writeBuffer(this.#cpuBuffer, 0, this.#bufferReset)
+ // Write data that will not change per dispatch to uniform buffer object
+ // Note: u32 size is 4, but total alignment must be multiple of 16
+ this.#uboArray.fill(0n)
+ for (let i = 0; i < 64; i += 16) {
+ this.#uboView.setBigUint64(i / 2, BigInt(`0x${hash.slice(i, i + 16)}`))
+ }
+ this.#uboView.setBigUint64(32, this.#difficulty, true)
+ this.#uboView.setUint32(40, this.#action === 'work_generate' ? 0 : 1, true)
+ this.#device.queue.writeBuffer(this.#uboBuffer, 0, this.#uboView)
+
+ this.#result = {
+ found: false,
+ work: 0n,
+ difficulty: 0n
+ }
+ }
+
+ static async #work_dispatch (seed: bigint, hash: string): Promise<void> {
+ if (this.#device == null) throw new Error(`WebGPU device failed to load.`)
+ if (this.#pipeline == null) throw new Error(`WebGPU pipeline failed to load.`)
+ if (this.#debug) console.log('seed', seed.toString(16).padStart(16, '0'))
+
+ // Copy seed into UBO
+ this.#uboView.setBigUint64(48, seed, true)
+ if (this.#debug) console.log('UBO', this.#uboView)
+ this.#device.queue.writeBuffer(this.#uboBuffer, 0, this.#uboView)
+
// Create command encoder to issue commands to GPU and initiate computation
const commandEncoder = this.#device.createCommandEncoder()
const passEncoder = commandEncoder.beginComputePass()
// Issue commands and end compute pass structure
- passEncoder.setPipeline(pipeline)
+ passEncoder.setPipeline(this.#pipeline)
passEncoder.setBindGroup(0, this.#bindGroup)
- passEncoder.dispatchWorkgroups(passes, passes)
+ passEncoder.dispatchWorkgroups(this.#effort, this.#effort)
passEncoder.end()
- // Copy 8-byte nonce and 4-byte found flag from GPU to CPU for reading
+ // Copy 8-byte result, 8-byte nonce, and 4-byte found flag from GPU to CPU
+ // for reading
commandEncoder.copyBufferToBuffer(this.#gpuBuffer, 0, this.#cpuBuffer, 0, 32)
// End computation by passing array of command buffers to command queue for execution
}
if (this.#debug) console.log('gpuBuffer data', this.#resultView)
if (this.#resultView == null) throw new Error(`Failed to get data from buffer.`)
+
+ this.#result.found = !!this.#resultView.getUint32(0)
+ this.#result.work = this.#resultView.getBigUint64(8, true)
+ this.#result.difficulty = this.#resultView.getBigUint64(16, true)
+
+ if (this.#debug) {
+ console.log('nonce', this.#result.work, this.#result.work.toString(16).padStart(16, '0'))
+ console.log('result', this.#result.difficulty, this.#result.difficulty.toString(16).padStart(16, '0'))
+ }
+ }
+
+ static #work_process (): void {
+ const { task, resolve, reject } = this.#queue.shift() ?? {}
+ task?.().then(resolve).catch(reject).finally(() => { this.#work_process() })
+ }
+
+ static async #work_queue (task: Function): Promise<any> {
+ return new Promise((resolve, reject): void => {
+ this.#queue.push({ task, resolve, reject })
+ this.#work_process()
+ })
}
/**
* @param {NanoPowOptions} options - Used to configure search execution
*/
static async work_generate (hash: string, options?: NanoPowOptions): Promise<WorkGenerateResponse> {
- if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new TypeError(`Invalid hash ${hash}`)
- if (this.#busy) {
- console.log('NanoPowGpu is busy. Retrying search...')
- return new Promise(resolve => {
- setTimeout(async (): Promise<void> => {
- const result = this.work_generate(hash, options)
- resolve(result)
- }, 100)
- })
- }
- if (this.#isInitialized === false) this.init()
- this.#busy = true
+ return this.#work_queue(async (): Promise<WorkGenerateResponse> => {
+ this.#action = 'work_generate'
+ await this.#work_init(null, hash, options)
- if (typeof options?.difficulty === 'string') {
- try {
- options.difficulty = BigInt(`0x${options.difficulty}`)
- } catch (err) {
- throw new TypeError(`Invalid difficulty ${options.difficulty}`)
- }
- }
- const difficulty = (typeof options?.difficulty !== 'bigint' || options.difficulty < 0n || options.difficulty > 0xffffffffffffffffn)
- ? 0xfffffff800000000n
- : options.difficulty
- const effort = (typeof options?.effort !== 'number' || options.effort < 0x1 || options.effort > 0x20)
- ? 0x800
- : options.effort * 0x100
- this.#debug = !!(options?.debug)
- if (this.#debug) console.log('NanoPowGpu.work_generate()')
- if (this.#debug) console.log('blockhash', hash)
- if (this.#debug) console.log('search options', JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
-
- // Ensure WebGPU is initialized before calculating
- let loads = 0
- while (this.#device == null && loads++ < 20) {
- await new Promise(resolve => {
- setTimeout(resolve, 500)
- })
- }
- if (this.#device == null) {
- this.#busy = false
- throw new Error(`WebGPU device failed to load.`)
- }
+ let random = BigInt(Math.floor(Math.random() * 0xffffffff))
+ let seed = random
+ do {
+ random = BigInt(Math.floor(Math.random() * 0xffffffff))
+ seed = (seed & 0xffffffffn) << 32n | random
+ await this.#work_dispatch(seed, hash)
+ } while (!this.#result.found)
- let times = []
- let start = performance.now()
- let nonce = 0n
- let result = 0n
- let random = BigInt(Math.floor(Math.random() * 0xffffffff))
- let seed = random
- do {
- start = performance.now()
- random = BigInt(Math.floor(Math.random() * 0xffffffff))
- seed = (seed & 0xffffffffn) << 32n | random
- if (this.#debug) console.log('seed', seed.toString(16).padStart(16, '0'))
- await this.#dispatch(this.#searchPipeline, seed, hash, difficulty, effort)
- const found = !!this.#resultView.getUint32(0)
- nonce = this.#resultView.getBigUint64(8, true)
- result = this.#resultView.getBigUint64(16, true)
- this.#busy = !found
- times.push(performance.now() - start)
- } while (this.#busy)
- if (this.#debug) this.#logAverages(times)
- if (this.#debug) console.log('nonce', nonce, nonce.toString(16).padStart(16, '0'))
- if (this.#debug) console.log('result', result, result.toString(16).padStart(16, '0'))
- return {
- hash,
- work: nonce.toString(16).padStart(16, '0'),
- difficulty: result.toString(16).padStart(16, '0')
- }
+ return {
+ hash,
+ work: this.#result.work.toString(16).padStart(16, '0'),
+ difficulty: this.#result.difficulty.toString(16).padStart(16, '0')
+ }
+ })
}
/**
* @param {NanoPowOptions} options - Options used to configure search execution
*/
static async work_validate (work: string, hash: string, options?: NanoPowOptions): Promise<WorkValidateResponse> {
- if (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new TypeError(`Invalid work ${work}`)
- if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new TypeError(`Invalid hash ${hash}`)
- if (this.#busy) {
- console.log('NanoPowGpu is busy. Retrying validate...')
- return new Promise(resolve => {
- setTimeout(async (): Promise<void> => {
- const result = this.work_validate(work, hash, options)
- resolve(result)
- }, 100)
- })
- }
- if (this.#isInitialized === false) this.init()
- this.#busy = true
+ return this.#work_queue(async (): Promise<WorkValidateResponse> => {
+ this.#action = 'work_validate'
+ await this.#work_init(work, hash, options)
- if (typeof options?.difficulty === 'string') {
- try {
- options.difficulty = BigInt(`0x${options.difficulty}`)
- } catch (err) {
- throw new TypeError(`Invalid difficulty ${options.difficulty}`)
- }
- }
- const difficulty = (typeof options?.difficulty !== 'bigint' || options.difficulty < 0n || options.difficulty > 0xffffffffffffffffn)
- ? 0xfffffff800000000n
- : options.difficulty
- this.#debug = !!(options?.debug)
- if (this.#debug) console.log('NanoPowGpu.work_validate()')
- if (this.#debug) console.log('blockhash', hash)
- if (this.#debug) console.log('validate options', JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
+ const seed = BigInt(`0x${work}`)
+ await this.#work_dispatch(seed, hash)
+ if (seed !== this.#result.work) throw new Error('Result does not match work')
- // Ensure WebGPU is initialized before calculating
- let loads = 0
- while (this.#device == null && loads < 20) {
- await new Promise(resolve => {
- setTimeout(resolve, 500)
- })
- }
- if (this.#device == null) {
- this.#busy = false
- throw new Error(`WebGPU device failed to load.`)
- }
-
- let result = 0n
- let nonce = 0n
-
- const seed = BigInt(`0x${work}`)
- if (this.#debug) console.log('work', work)
- await this.#dispatch(this.#validatePipeline, seed, hash, difficulty, 1)
- nonce = this.#resultView.getBigUint64(8, true)
- result = this.#resultView.getBigUint64(16, true)
- this.#busy = false
- if (seed !== nonce) throw new Error('Result does not match work')
- if (this.#debug) console.log('nonce', nonce, nonce.toString(16).padStart(16, '0'))
- if (this.#debug) console.log('result', result, result.toString(16).padStart(16, '0'))
- const response: WorkValidateResponse = {
- hash,
- work: nonce.toString(16).padStart(16, '0'),
- difficulty: result.toString(16).padStart(16, '0'),
- valid_all: (result >= this.#SEND) ? '1' : '0',
- valid_receive: (result >= this.#RECEIVE) ? '1' : '0',
- }
- if (options?.difficulty != null) response.valid = (result >= difficulty) ? '1' : '0'
- return response
+ const response: WorkValidateResponse = {
+ hash,
+ work: this.#result.work.toString(16).padStart(16, '0'),
+ difficulty: this.#result.difficulty.toString(16).padStart(16, '0'),
+ valid_all: (this.#result.difficulty >= this.#SEND) ? '1' : '0',
+ valid_receive: (this.#result.difficulty >= this.#RECEIVE) ? '1' : '0'
+ }
+ if (options?.difficulty != null) {
+ response.valid = (this.#result.difficulty >= this.#difficulty) ? '1' : '0'
+ }
+ return response
+ })
}
}
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { NanoPow, NanoPowGl, NanoPowGpu } from './lib'
-export { NanoPow, NanoPowGl, NanoPowGpu }
-export default NanoPow
+export { NanoPow as default, NanoPow, NanoPowGl, NanoPowGpu } from './lib'
* Nano proof-of-work using WebGL 2.0.
*/
export declare class NanoPowGl {
- static [key: string]: (...args: any[]) => any
#private
/**
* Constructs canvas, gets WebGL context, initializes buffers, and compiles
*/
export declare class NanoPowGpu {
#private
- static [key: string]: (...args: any[]) => any
static init (): Promise<void>
- static setup (): void
static reset (): void
/**
* Finds a nonce that satisfies the Nano proof-of-work requirements.
difficulty?: bigint | string
}
+export type NanoPowResult = {
+ found: boolean
+ work: bigint
+ difficulty: bigint
+}
+
/**
* Used to create WebGL framebuffer objects.
*
-* @param {WebGLTexture} - Defines storage size
-* @param {WebGLFramebuffer} - Holds texture data
-* @param {size} - 2D lengths of texture
+* @param {WebGLTexture} texture - Defines storage size
+* @param {WebGLFramebuffer} framebuffer - Holds texture data
+* @param {size} size - 2D lengths of texture
*/
export type FBO = {
texture: WebGLTexture
}
}
+/**
+* Used to define NanoPow server configuration.
+*
+* @param {boolean} DEBUG - Enables additional log output
+* @param {number} EFFORT - Defines dispatch size per compute pass
+* @param {number} PORT - TCP port on which to listen for requests
+*/
+export type NanoPowServerConfig = {
+ [key: string]: boolean | number
+ DEBUG: boolean
+ EFFORT: number
+ PORT: number
+}
+
export declare const NanoPowGlDownsampleShader: string
export declare const NanoPowGlDrawShader: string
export declare const NanoPowGlVertexShader: string
--- /dev/null
+export function average (times: number[]) {
+ if (times == null || times.length === 0) return {}
+
+ let count = times.length
+ let min = Number.MAX_SAFE_INTEGER
+ let logarithms, max, median, rate, reciprocals, total, truncated, truncatedCount
+ logarithms = max = median = rate = reciprocals = total = truncated = truncatedCount = 0
+
+ times.sort((a, b) => a - b)
+ for (let i = 0; i < count; i++) {
+ const time = times[i]
+ total += time
+ reciprocals += 1 / time
+ logarithms += Math.log(time)
+ min = Math.min(min, time)
+ max = Math.max(max, time)
+ if (i + 1 === Math.ceil(count / 2)) median = time
+ if (count < 3 || (i > (0.1 * count) && i < (0.9 * (count - 1)))) {
+ truncated += time
+ truncatedCount++
+ }
+ }
+ return {
+ count,
+ total,
+ min,
+ max,
+ median,
+ arithmetic: total / count,
+ harmonic: count / reciprocals,
+ geometric: Math.exp(logarithms / count),
+ truncated: truncated / truncatedCount,
+ rate: 1000 * truncatedCount / (truncated || total),
+ }
+}
+
+export function isHex (input: string, min?: number, max?: number): boolean {
+ if (typeof input !== 'string') {
+ return false
+ }
+ if (typeof min !== 'undefined' && typeof min !== 'number') {
+ throw new Error(`Invalid argument for parameter 'min'`)
+ }
+ if (typeof max !== 'undefined' && typeof max !== 'number') {
+ throw new Error(`Invalid argument for parameter 'max'`)
+ }
+ const range = min === undefined && max === undefined
+ ? '+'
+ : `{${min ?? '0'},${max ?? ''}}`
+ const regexp = new RegExp(`^[0-9A-Fa-f]${range}$`, 'm')
+ return regexp.test(input)
+}
+
+export function isNotHex (input: string, min?: number, max?: number): boolean {
+ return !isHex(input, min, max)
+}
+
+/**
+* Override console logging to provide an informative prefix for each entry and
+* to only output when debug mode is enabled.
+*/
+export function log (...args: any[]): void {
+ if (process?.env?.NANO_POW_DEBUG) {
+ const entry = `${new Date(Date.now()).toLocaleString(Intl.DateTimeFormat().resolvedOptions().locale ?? 'en-US', { hour12: false, dateStyle: 'medium', timeStyle: 'medium' })} NanoPow[${process.pid}]: ${args}`
+ console.log(entry)
+ process.send?.({ type: 'console', message: entry })
+ }
+}
function average (times, type, effort) {
let count = times.length, truncatedCount = 0, sum = 0, truncated = 0, reciprocals = 0, logarithms = 0, min = Number.MAX_SAFE_INTEGER, max = 0, median = 0, rate = 0
- times.sort()
+ times.sort((a, b) => a - b)
for (let i = 0; i < count; i++) {
sum += times[i]
reciprocals += 1 / times[i]
console.log(`work_validate() output for good nonce 2 is ${result === true ? 'correct' : 'incorrect'}`)
expect.push(result)
- result = await NP.work_validate('326f310d629a8a98', '204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA', { difficulty: 0xffffffff00000000n, debug: isDebug })
- result = result.valid === '1' && result.valid_all === '1' && result.valid_receive === '1'
- console.log(`work_validate() output for good max difficulty nonce is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result)
-
result = await NP.work_validate('c5d5d6f7c5d6ccd1', '281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117', { debug: isDebug })
result = result.valid_all === '1'
console.log(`work_validate() output for colliding nonce is ${result === true ? 'correct' : 'incorrect'}`)
console.log(`work_validate() output for bad nonce 2 is ${result === true ? 'correct' : 'incorrect'}`)
expect.push(result)
- result = await NP.work_validate('ae238556213c3624', 'BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6', { difficulty: 0xffffffff00000000n, debug: isDebug })
- result = result.valid === '0' && result.valid_all === '0' && result.valid_receive === '1'
- console.log(`work_validate() output for bad max difficulty nonce is ${result === true ? 'correct' : 'incorrect'}`)
+ try {
+ result = await NP.work_validate('ae238556213c3624', 'BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6', { difficulty: 0xfffffff800000001n, debug: isDebug })
+ console.log('boo')
+ } catch (err) {
+ result = null
+ }
+ result = result === null
+ console.log(`work_validate() output for bad difficulty beyond max is ${result === true ? 'correct' : 'incorrect'}`)
expect.push(result)
result = await NP.work_validate('29a9ae0236990e2e', '32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F', { debug: isDebug })
console.log(`work_validate() output for bad receive difficulty nonce is ${result === true ? 'correct' : 'incorrect'}`)
expect.push(result)
- result = await NP.work_validate('e45835c3b291c3d1', '9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F', { difficulty: 0xffffffff00000000n, debug: isDebug })
- result = result.valid === '0' && result.valid_all === '1' && result.valid_receive === '1'
- console.log(`work_validate() output for send difficulty nonce that does not meet custom difficulty is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result)
-
try {
if (!expect.every(result => result)) throw new Error(`Validation is not working`)
} catch (err) {
const check = await NP.work_validate(result.work, result.hash, { difficulty, debug: isDebug })
const isValid = (result.hash === hash && check.valid === '1') ? 'VALID' : 'INVALID'
times.push(end - start)
- const msg = `${isValid} [${result.work}] ${result.hash} (${end - start} ms)`
+ const msg = `${isValid} (${end - start} ms)\n${JSON.stringify(result, ' ', 2)}`
if (isOutputShown) document.getElementById('output').innerHTML += `${msg}<br/>`
}
document.getElementById('output').innerHTML += `<hr/>`
run(difficulty.value, +size.value, +effort.value, isOutputShown.checked, isGlForced.checked, isDebug.checked)
}
document.getElementById('btnStartTest').addEventListener('click', startTest)
- document.getElementById('effort').value = Math.max(1, Math.floor(navigator.hardwareConcurrency))
+ document.getElementById('effort').value = Math.max(1, Math.floor(navigator.hardwareConcurrency) / 2)
</script>
<style>
body{background:black;color:white;}a{color:darkcyan;}input[type=number]{width:5em;}span{margin:0.5em;}
+#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
# SPDX-License-Identifier: GPL-3.0-or-later
export NANO_POW_PORT=3001
"$SCRIPT_DIR"/../dist/bin/nano-pow.sh --server
-sleep 2s
+sleep 3s
printf '\nGet documentation\n'
curl 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
+curl -d '{ "action": "work_validate", "work": "47c83266398728cf", "hash": "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D", "difficulty": "-1", "foo": "bar" }' localhost:3001
printf '\nValidate good hashes\n'
curl -d '{ "action": "work_validate", "work": "47c83266398728cf", "hash": "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D" }' localhost:3001
curl -d '{ "action": "work_validate", "work": "4a8fb104eebbd336", "hash": "8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01" }' localhost:3001
curl -d '{ "action": "work_validate", "work": "c5d5d6f7c5d6ccd1", "hash": "281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117" }' localhost:3001
-curl -d '{ "action": "work_validate", "work": "326f310d629a8a98", "hash": "204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA", "difficulty": "ffffffff00000000" }' localhost:3001
+curl -d '{ "action": "work_validate", "work": "326f310d629a8a98", "hash": "204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA", "difficulty": "fffffff700000000" }' localhost:3001
curl -d '{ "action": "work_validate", "work": "6866c1ac3831a891", "hash": "7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090", "difficulty": "fffffe0000000000" }' localhost:3001
printf '\nValidate bad hashes\n'
curl -d '{ "action": "work_validate", "work": "0000000000000000", "hash": "0000000000000000000000000000000000000000000000000000000000000000" }' localhost:3001
curl -d '{ "action": "work_validate", "work": "c5d5d6f7c5d6ccd1", "hash": "BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E" }' localhost:3001
curl -d '{ "action": "work_validate", "work": "29a9ae0236990e2e", "hash": "32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F" }' localhost:3001
-curl -d '{ "action": "work_validate", "work": "ae238556213c3624", "hash": "BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6", "difficulty": "ffffffff00000000" }' localhost:3001
+curl -d '{ "action": "work_validate", "work": "ae238556213c3624", "hash": "BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6", "difficulty": "fffffff700000000" }' localhost:3001
curl -d '{ "action": "work_validate", "work": "7d903b18d03f9820", "hash": "39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150", "difficulty": "fffffe0000000000" }' localhost:3001
-curl -d '{ "action": "work_validate", "work": "e45835c3b291c3d1", "hash": "9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F", "difficulty": "ffffffff00000000" }' localhost:3001
-
+curl -d '{ "action": "work_validate", "work": "e45835c3b291c3d1", "hash": "9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F", "difficulty": "fffffff700000000" }' localhost:3001
printf '\nGenerate\n'
-curl -d '{ "action": "work_generate", "hash": "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D" }' localhost:3001 &
-curl -d '{ "action": "work_generate", "hash": "8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01" }' localhost:3001 &
-curl -d '{ "action": "work_generate", "hash": "281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117" }' localhost:3001 &
-curl -d '{ "action": "work_generate", "hash": "204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA", "difficulty": "ffffffff00000000" }' localhost:3001 &
-curl -d '{ "action": "work_generate", "hash": "7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090", "difficulty": "fffffe0000000000" }' localhost:3001 &
-wait
+curl -d '{ "action": "work_generate", "hash": "0D653EBFE692CA655449B301DBCEEF4E4A47454E3DFDF8C87B87C50DB6530A25" }' localhost:3001
+curl -d '{ "action": "work_generate", "hash": "E93104CFC1CE40B2A29F0E85807E7E51E521C30E2B79A8EDDDB2D771DCF5C06B" }' localhost:3001
+curl -d '{ "action": "work_generate", "hash": "472F8F5D908AF51E9583950D50BAE6A8C5FD66D6984FA62D4D31ADDEA5AA3AC7" }' localhost:3001
+curl -d '{ "action": "work_generate", "hash": "B349DA23CB6979DF5894C22C427D19DD8B54B94F1F9A1F82EF74495993C750D7", "difficulty": "fffffff700000000" }' localhost:3001
+curl -d '{ "action": "work_generate", "hash": "93C2252F4D4F3EF84BFF88233289046F82245245C00A064C370BAF9889824DE9", "difficulty": "fffffe0000000000" }' localhost:3001
+
kill $(cat "$HOME"/.nano-pow/server.pid) && rm "$HOME"/.nano-pow/server.pid
"paths": {
"#types": [
"./src/types.d.ts"
+ ],
+ "#utils": [
+ "./src/utils/index.ts"
]
},
"types": [