Add executable start a Node server, accept POST requests in same JSON format as Nano node specs, and process similarly to CLI using puppeteer. Remove migration compatibility layer introduced in v3.2.0. Extract help text to separate documentation file. Write basic test script to check server. Refactor threshold to expect 64-bit values only. Fix Typescript types.
# IDE\r
.vs/\r
.vscode/\r
+\r
+# Server process ID\r
+server.pid\r
--- /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 format output as JSON
+ -e, --effort=<value> increase demand on GPU processing
+ -t, --threshold=<value> override the minimum threshold value
+ -v, --validate=<value> check an existing work value instead of searching for one
+
+If validating a nonce, it must be a 16-character hexadecimal value.
+Effort must be a decimal number between 1-32.
+Threshold must be a hexadecimal string between 0-FFFFFFFF.
+
+Report bugs: <bug-nano-pow@zoso.dev>
+Full documentation: <https://www.npmjs.com/package/nano-pow>
+`
+
+export const serverHelp = `Usage: Send POST request to server URL to generate or validate Nano proof-of-work
+
+Generate work for a BLOCKHASH with an optional DIFFICULTY:
+ curl -d '{ action: "work_generate", hash: BLOCKHASH, difficulty?: DIFFICULTY }'
+
+Validate WORK previously calculated for a BLOCKHASH with an optional DIFFICULTY:
+ curl -d '{ action: "work_validate", work: WORK, hash: BLOCKHASH, difficulty?: DIFFICULTY }'
+
+BLOCKHASH is a 64-character hexadecimal string.
+WORK is 16-character hexadecimal string.
+DIFFICULTY is a 16-character hexadecimal string (default: FFFFFFF800000000)
+
+Report bugs: <bug-nano-pow@zoso.dev>
+Full documentation: <https://www.npmjs.com/package/nano-pow>
+`
"url": "git+https://zoso.dev/nano-pow.git"
},
"scripts": {
- "build": "rm -rf {dist,types} && tsc && node esbuild.mjs"
+ "build": "rm -rf {dist,types} && tsc && node esbuild.mjs",
+ "start": "mkdir -p logs; node ./dist/bin/server.js > ./logs/nano-pow-server-$(date +%s).log 2>&1 & echo $! > server.pid",
+ "test": "./test/script.sh"
},
"devDependencies": {
"@types/node": "^22.13.11",
import * as fs from 'node:fs/promises'
import * as readline from 'node:readline/promises'
import * as puppeteer from 'puppeteer'
+import { cliHelp } from '../../docs/index.js'
const hashes: string[] = []
const args = process.argv.slice(2)
if ((hashes.length === 0 && args.length === 0) || (args.some(v => v === '--help' || v === '-h'))) {
- console.log(`Usage: nano-pow [OPTION]... BLOCKHASH...
-Generate work for BLOCKHASH, or multiple work values for BLOCKHASH(es)
-BLOCKHASH is a 64-character hexadecimal string. Multiple blockhashes must be separated by whitespace or line breaks.
-Prints a 16-character hexadecimal work value to standard output. If using --validate, prints 'true' or 'false' to standard output instead.
-
- -h, --help show this dialog
- -d, --debug enable additional logging output
- -j, --json format output as JSON
- -e, --effort=<value> increase demand on GPU processing
- -t, --threshold=<value> override the minimum threshold value
- -v, --validate=<value> check an existing work value instead of searching for one
-
-If validating a nonce, it must be a 16-character hexadecimal value.
-Effort must be a decimal number between 1-32.
-Threshold must be a hexadecimal string between 0-FFFFFFFF.
-
-Report bugs: <bug-nano-pow@zoso.dev>
-Full documentation: <https://www.npmjs.com/package/nano-pow>
-`)
+ console.log(cliHelp)
process.exit()
}
}
hashes.push(...inArgs)
-let fn = 'search'
+let fn = 'work_generate'
let work = ''
let isJson = false
const options = {}
case ('-v'): {
if (args[i + 1] == null) throw new Error('Missing argument for work validation')
if (!/^[0-9A-Fa-f]{16}$/.test(args[i + 1])) throw new Error('Invalid work to validate')
- fn = 'validate'
+ fn = 'work_validate'
work = `'${args[i + 1]}', `
break
}
/**
* Main
*/
-(async () => {
+(async (): Promise<void> => {
const NanoPow = await fs.readFile(new URL('../main.min.js', import.meta.url), 'utf-8')
const browser = await puppeteer.launch({
headless: true,
const cliPage = `${import.meta.dirname}/cli.html`
await fs.writeFile(cliPage, '')
await page.goto(import.meta.resolve('./cli.html'))
- await page.waitForFunction(async () => {
+ await page.waitForFunction(async (): Promise<GPUAdapter | null> => {
return await navigator.gpu.requestAdapter()
})
const hashes = ["${hashes.join('","')}"]
for (const hash of hashes) {
try {
- const work = await NanoPow.${fn}(${work}hash, ${JSON.stringify(options)})
- window.results.push(work)
- console.log(\`cli \${work}\`)
+ const result = await NanoPow.${fn}(${work}hash, ${JSON.stringify(options)})
+ window.results.push(result)
+ console.log(\`cli \${JSON.stringify(result, null, 4)}\`)
} catch (err) {
console.error(\`cli \${err}\`)
}
const src = `sha256-${Buffer.from(hash).toString('base64')}`
let start = performance.now()
- page.on('console', async (msg) => {
- const output = msg.text().split(' ')
- if (output[0] === 'cli') {
+ page.on('console', async (msg): Promise<void> => {
+ const output = msg.text().split(/^cli /)
+ if (output[0] === '') {
if (output[1] === 'exit') {
if (isJson) {
- const results = await page.evaluate(() => {
+ const results = await page.evaluate((): any => {
return (window as any).results
})
- for (let i = 0; i < results.length; i++) {
- results[i] = {
- blockhash: hashes[i],
- work: results[i]
- }
- }
console.log(JSON.stringify(results, null, 4))
}
const end = performance.now()
if (options['debug']) console.log(end - start, 'ms total |', (end - start) / hashes.length, 'ms avg')
await browser.close()
} else if (!isJson) {
- console.log(output[1])
+ try {
+ console.log(JSON.parse(output[1]))
+ } catch (err) {
+ console.log(output[1])
+ }
}
} else if (options['debug']) {
- console.log(msg.text())
+ try {
+ console.log(JSON.parse(msg.text()))
+ } catch (err) {
+ console.log(msg.text())
+ }
}
})
start = performance.now()
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import * as http from 'node:http'
+import * as fs from 'node:fs/promises'
+import * as puppeteer from 'puppeteer'
+import { serverHelp } from '../../docs/index.js'
+import { NanoPowOptions, WorkGenerateRequest, WorkGenerateResponse, WorkValidateRequest, WorkValidateResponse } from '../types.js'
+
+const PORT = process.env.PORT || 3000
+
+function log (...args) {
+ console.log(new Date(Date.now()).toLocaleString(), 'NanoPow', args)
+}
+
+log('Starting server')
+
+const NanoPow = await fs.readFile(new URL('../main.min.js', import.meta.url), 'utf-8')
+
+// Launch puppeteer browser instance - Persistent instance
+let browser: puppeteer.Browser
+let page: puppeteer.Page
+
+async function work_generate (res: http.ServerResponse, json: WorkGenerateRequest): Promise<void> {
+ if (!/^[0-9A-Fa-f]{64}$/.test(json.hash ?? '')) {
+ res.writeHead(400, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Invalid hash. Must be a 64-character hex string.' }))
+ return
+ }
+ if (json.difficulty && !/^[1-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(json.difficulty)) {
+ res.writeHead(400, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Invalid difficulty. Must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF.' }))
+ return
+ }
+
+ try {
+ const result = await page.evaluate(async (args: WorkGenerateRequest): Promise<WorkGenerateResponse> => {
+ const options: NanoPowOptions = {
+ debug: true
+ }
+ if (args.difficulty) options.threshold = BigInt(`0x${args.difficulty}`)
+ // @ts-expect-error
+ return await window.NanoPow.work_generate(args.hash, options)
+ }, json)
+ res.writeHead(200, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify(result))
+ } catch (err) {
+ log('work_generate error:', err)
+ res.writeHead(500, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'work_generate failed' }))
+ }
+}
+
+async function work_validate (res: http.ServerResponse, json: WorkValidateRequest): Promise<void> {
+ if (!/^[0-9A-Fa-f]{64}$/.test(json.hash ?? '')) {
+ res.writeHead(400, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Invalid hash. Must be a 64-character hex string.' }))
+ return
+ }
+ if (json.difficulty && !/^[1-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(json.difficulty)) {
+ res.writeHead(400, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Invalid difficulty. Must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF.' }))
+ return
+ }
+ if (!/^[0-9A-Fa-f]{16}$/.test(json.work ?? '')) {
+ res.writeHead(400, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Invalid work. Must be a 16-character hex string.' }))
+ return
+ }
+
+ try {
+ const result: WorkValidateResponse = await page.evaluate(async (args: WorkValidateRequest): Promise<WorkValidateResponse> => {
+ const options: NanoPowOptions = {
+ debug: true
+ }
+ if (args.difficulty) options.threshold = BigInt(`0x${args.difficulty}`)
+ //@ts-expect-error
+ return await window.NanoPow.work_validate(args.work, args.hash, options)
+ }, json)
+ res.writeHead(200, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify(result))
+ } catch (err) {
+ log('work_validate error:', err)
+ res.writeHead(500, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'work_validate failed' }))
+ }
+}
+
+// Start server
+(async (): Promise<void> => {
+ // Initialize puppeteer
+ browser = await puppeteer.launch({
+ headless: true,
+ args: [
+ '--headless=new',
+ '--use-angle=vulkan',
+ '--enable-features=Vulkan',
+ '--disable-vulkan-surface',
+ '--enable-unsafe-webgpu'
+ ]
+ })
+ page = await browser.newPage()
+ page.on('console', (msg): void => {
+ log(msg.text())
+ })
+ await fs.writeFile(`${import.meta.dirname}/server.html`, '')
+ await page.goto(import.meta.resolve('./server.html'))
+ await page.waitForFunction(async (): Promise<GPUAdapter | null> => {
+ return await navigator.gpu.requestAdapter()
+ })
+
+ const inject = `${NanoPow};window.NanoPow=NanoPow;`
+ const hash = await crypto.subtle.digest('SHA-256', Buffer.from(inject, 'utf-8'))
+ const src = `sha256-${Buffer.from(hash).toString('base64')}`
+
+ await page.setContent(`
+ <!DOCTYPE html>
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'; script-src '${src}';">
+ <script type="module">${inject}</script>
+ </head>
+ </html>
+ `)
+ await fs.unlink(`${import.meta.dirname}/server.html`)
+ log('Puppeteer initialized')
+
+ // Create server
+ const server = http.createServer(async (req, res): Promise<void> => {
+ let data: Buffer[] = []
+ if (req.method === 'POST') {
+ req.on('data', (chunk: Buffer): void => {
+ data.push(chunk)
+ })
+ req.on('end', async (): Promise<void> => {
+ let json
+ try {
+ json = JSON.parse(Buffer.concat(data).toString())
+ } catch (err) {
+ log('JSON.parse error:', err)
+ log('Failed JSON:', json)
+ res.writeHead(400, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Invalid data.' }))
+ return
+ }
+ switch (json.action) {
+ case ('work_generate'): {
+ await work_generate(res, json)
+ break
+ }
+ case ('work_validate'): {
+ await work_validate(res, json)
+ break
+ }
+ default: {
+ res.writeHead(400, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: `Invalid data.` }))
+ return
+ }
+ }
+ })
+ } else {
+ res.writeHead(200, { 'Content-Type': 'text/plain' })
+ res.end(serverHelp)
+ }
+ })
+
+ server.on('error', (e): void => {
+ log('Server error', e)
+ try {
+ shutdown()
+ } catch (err) {
+ log('Failed to shut down', err)
+ process.exit(1)
+ }
+ })
+
+ // Listen on configured port
+ server.listen(PORT, (): void => {
+ process.title = 'NanoPow Server'
+ log(`Server process ${process.pid} running at http://localhost:${PORT}/`)
+ })
+
+ // Shut down server gracefully when process is terminated
+ function shutdown (): void {
+ log('Shutdown signal received')
+ const kill = setTimeout((): never => {
+ log('Server unresponsive, forcefully stopped')
+ process.exit(1)
+ }, 10000)
+ server.close(async (): Promise<never> => {
+ await page?.close()
+ await browser?.close()
+ clearTimeout(kill)
+ log('Server stopped')
+ process.exit(0)
+ })
+ }
+ process.on('SIGINT', shutdown)
+ process.on('SIGTERM', shutdown)
+})()
throw new Error('Query reported result but nonce value not found')
}
- /**
- * Finds a nonce that satisfies the Nano proof-of-work requirements.
- *
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
- * @param {NanoPowOptions} options - Used to configure search execution
- */
- static async search (hash: string, options?: NanoPowOptions): Promise<string> {
- if (options?.threshold != null) {
- options.threshold = BigInt(`0x${options.threshold.toString(16) ?? '0'}00000000`)
- }
- const result = await this.work_generate(hash, options)
- return result.work
- }
-
/**
* Finds a nonce that satisfies the Nano proof-of-work requirements.
*
* @param {NanoPowOptions} options - Options used to configure search execution
*/
static async work_generate (hash: string, options?: NanoPowOptions): Promise<WorkGenerateResponse> {
+ if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`)
if (this.#busy) {
console.log('NanoPowGl is busy. Retrying search...')
return new Promise(resolve => {
}
this.#busy = true
- /** Process user input */
- if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`)
+ if (typeof options?.threshold === 'string') {
+ try {
+ options.threshold = BigInt(`0x${options.threshold}`)
+ } catch (err) {
+ throw new TypeError(`Invalid threshold ${options.threshold}`)
+ }
+ }
const threshold = (typeof options?.threshold !== 'bigint' || options.threshold < 1n || options.threshold > 0xffffffffffffffffn)
? 0xfffffff800000000n
: options.threshold
? this.#cores
: options.effort
this.#debug = !!(options?.debug)
- if (this.#debug) console.log('NanoPowGl.search()')
+ if (this.#debug) console.log('NanoPowGl.work_generate()')
if (this.#debug) console.log('blockhash', hash)
if (this.#debug) console.log('search options', JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
}
}
- /**
- * Validates that a nonce satisfies Nano proof-of-work requirements.
- *
- * @param {string} work - Hexadecimal proof-of-work value to validate
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
- * @param {NanoPowOptions} options - Options used to configure search execution
- */
- static async validate (work: string, hash: string, options?: NanoPowOptions): Promise<boolean> {
- if (options?.threshold != null) {
- options.threshold = BigInt(`0x${options.threshold.toString(16) ?? '0'}00000000`)
- }
- const result = await this.work_validate(work, hash, options)
- return (options?.threshold != null)
- ? result.valid === '1'
- : result.valid_all === '1'
- }
-
/**
* Validates that a nonce satisfies Nano proof-of-work requirements.
*
* @param {NanoPowOptions} options - Options used to configure search execution
*/
static async work_validate (work: string, hash: string, options?: NanoPowOptions): Promise<WorkValidateResponse> {
+ if (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new Error(`Invalid work ${work}`)
+ if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`)
if (this.#busy) {
console.log('NanoPowGl is busy. Retrying validate...')
return new Promise(resolve => {
}
this.#busy = true
- /** Process user input */
- if (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new Error(`Invalid work ${work}`)
- if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`)
+ if (typeof options?.threshold === 'string') {
+ try {
+ options.threshold = BigInt(`0x${options.threshold}`)
+ } catch (err) {
+ throw new TypeError(`Invalid threshold ${options.threshold}`)
+ }
+ }
const threshold = (typeof options?.threshold !== 'bigint' || options.threshold < 1n || options.threshold > 0xffffffffffffffffn)
? 0xfffffff800000000n
: options.threshold
this.#debug = !!(options?.debug)
- if (this.#debug) console.log('NanoPowGl.validate()')
+ if (this.#debug) console.log('NanoPowGl.work_validate()')
if (this.#debug) console.log('blockhash', hash)
if (this.#debug) console.log('validate options', JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
return data
}
- /**
- * Finds a nonce that satisfies the Nano proof-of-work requirements.
- *
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
- * @param {NanoPowOptions} options - Used to configure search execution
- */
- static async search (hash: string, options?: NanoPowOptions): Promise<string> {
- if (options?.threshold != null) {
- options.threshold = BigInt(`0x${options.threshold.toString(16) ?? '0'}00000000`)
- }
- const result = await this.work_generate(hash, options)
- return result.work
- }
-
/**
* Finds a nonce that satisfies the Nano proof-of-work requirements.
*
})
}
this.#busy = true
+
+ if (typeof options?.threshold === 'string') {
+ try {
+ options.threshold = BigInt(`0x${options.threshold}`)
+ } catch (err) {
+ throw new TypeError(`Invalid threshold ${options.threshold}`)
+ }
+ }
const threshold = (typeof options?.threshold !== 'bigint' || options.threshold < 1n || options.threshold > 0xffffffffffffffffn)
? 0xfffffff800000000n
: options.threshold
? 0x800
: options.effort * 0x100
this.#debug = !!(options?.debug)
- if (this.#debug) console.log('NanoPowGpu.search()')
+ if (this.#debug) console.log('NanoPowGpu.work_generate()')
if (this.#debug) console.log('blockhash', hash)
if (this.#debug) console.log('search options', JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
}
}
- /**
- * Validates that a nonce satisfies Nano proof-of-work requirements.
- *
- * @param {string} work - Hexadecimal proof-of-work value to validate
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
- * @param {NanoPowOptions} options - Options used to configure search execution
- */
- static async validate (work: string, hash: string, options?: NanoPowOptions): Promise<boolean> {
- if (options?.threshold != null) {
- options.threshold = BigInt(`0x${options.threshold.toString(16) ?? '0'}00000000`)
- }
- const result = await this.work_validate(work, hash, options)
- return (options?.threshold != null)
- ? result.valid === '1'
- : result.valid_all === '1'
- }
-
/**
* Validates that a nonce satisfies Nano proof-of-work requirements.
*
})
}
this.#busy = true
+
+ if (typeof options?.threshold === 'string') {
+ try {
+ options.threshold = BigInt(`0x${options.threshold}`)
+ } catch (err) {
+ throw new TypeError(`Invalid threshold ${options.threshold}`)
+ }
+ }
const threshold = (typeof options?.threshold !== 'bigint' || options.threshold < 1n || options.threshold > 0xffffffffffffffffn)
? 0xfffffff800000000n
: options.threshold
this.#debug = !!(options?.debug)
- if (this.#debug) console.log('NanoPowGpu.validate()')
+ if (this.#debug) console.log('NanoPowGpu.work_validate()')
if (this.#debug) console.log('blockhash', hash)
if (this.#debug) console.log('validate options', JSON.stringify(options, (k, v) => typeof v === 'bigint' ? v.toString(16) : v))
*
* @param {boolean} [debug=false] - Enables additional debug logging to the console. Default: false
* @param {number} [effort=0x8] - Multiplier for dispatching work search. Larger values are not necessarily better since they can quickly overwhelm the GPU. Ignored when validating. Default: 0x8
-* @param {number} [threshold=0xfffffff8] - Minimum value result of `BLAKE2b(nonce||blockhash) << 0x32`. Default: 0xFFFFFFF8
+* @param {bigint|string} [threshold=0xfffffff800000000] - Minimum value result of `BLAKE2b(nonce||blockhash)`. Default: 0xFFFFFFF800000000
*/
export type NanoPowOptions = {
debug?: boolean
effort?: number
- threshold?: bigint | number
+ threshold?: bigint | string
}
/**
export declare class NanoPowGl {
#private
/** Drawing buffer width in pixels. */
- static get size (): number | undefined
+ static get size (): number
/**
* Constructs canvas, gets WebGL context, initializes buffers, and compiles
* shaders.
export async function run (threshold, size, effort, isOutputShown, isGlForced, isDebug) {
const NP = isGlForced ? NanoPowGl : NanoPow
const type = (NP === NanoPowGpu) ? 'WebGPU' : (NP === NanoPowGl) ? 'WebGL' : 'unknown API'
- document.getElementById('status').innerHTML = `TESTING IN PROGRESS 0/${size}`
- console.log(`%cNanoPow`, 'color:green', 'Checking validate()')
+ console.log(`%cNanoPow`, 'color:green', 'Checking validation against known values')
const expect = []
let result
// PASS
- result = await NP.validate('47c83266398728cf', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', { debug: isDebug })
- console.log(`validate() output for good nonce 1 is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result === true)
+ result = await NP.work_validate('47c83266398728cf', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', { debug: isDebug })
+ result = result.valid_all === '1'
+ console.log(`work_validate() output for good nonce 1 is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
- result = await NP.validate('4a8fb104eebbd336', '8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01', { debug: isDebug })
- console.log(`validate() output for good nonce 2 is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result === true)
+ result = await NP.work_validate('4a8fb104eebbd336', '8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01', { debug: isDebug })
+ result = result.valid_all === '1'
+ console.log(`work_validate() output for good nonce 2 is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
- result = await NP.validate('326f310d629a8a98', '204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA', { threshold: 0xffffffff, debug: isDebug })
- console.log(`validate() output for good max threshold nonce is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result === true)
+ result = await NP.work_validate('326f310d629a8a98', '204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA', { threshold: 0xffffffff00000000n, debug: isDebug })
+ result = result.valid === '1' && result.valid_all === '1' && result.valid_receive === '1'
+ console.log(`work_validate() output for good max threshold nonce is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
- result = await NP.validate('c5d5d6f7c5d6ccd1', '281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117', { debug: isDebug })
- console.log(`validate() output for colliding nonce is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result === true)
+ result = await NP.work_validate('c5d5d6f7c5d6ccd1', '281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117', { debug: isDebug })
+ result = result.valid_all === '1'
+ console.log(`work_validate() output for colliding nonce is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
- result = await NP.validate('6866c1ac3831a891', '7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090', { threshold: 0xfffffe00, debug: isDebug })
- console.log(`validate() output for good receive threshold nonce is ${result === true ? 'correct' : 'incorrect'}`)
- expect.push(result === true)
+ result = await NP.work_validate('6866c1ac3831a891', '7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090', { threshold: 0xfffffe0000000000n, debug: isDebug })
+ result = result.valid === '1' && result.valid_all === '0' && result.valid_receive === '1'
+ console.log(`work_validate() output for good receive threshold nonce is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
// XFAIL
- result = await NP.validate('0000000000000000', '0000000000000000000000000000000000000000000000000000000000000000', { debug: isDebug })
- console.log(`validate() output for bad nonce 1 is ${result === false ? 'correct' : 'incorrect'}`)
- expect.push(result === false)
+ result = await NP.work_validate('0000000000000000', '0000000000000000000000000000000000000000000000000000000000000000', { debug: isDebug })
+ result = result.valid_all === '0'
+ console.log(`work_validate() output for bad nonce 1 is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
- result = await NP.validate('c5d5d6f7c5d6ccd1', 'BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E', { debug: isDebug })
- console.log(`validate() output for bad nonce 2 is ${result === false ? 'correct' : 'incorrect'}`)
- expect.push(result === false)
+ result = await NP.work_validate('c5d5d6f7c5d6ccd1', 'BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E', { debug: isDebug })
+ result = result.valid_all === '0'
+ console.log(`work_validate() output for bad nonce 2 is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
- result = await NP.validate('ae238556213c3624', 'BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6', { threshold: 0xffffffff, debug: isDebug })
- console.log(`validate() output for bad max threshold nonce is ${result === false ? 'correct' : 'incorrect'}`)
- expect.push(result === false)
+ result = await NP.work_validate('ae238556213c3624', 'BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6', { threshold: 0xffffffff00000000n, debug: isDebug })
+ result = result.valid === '0' && result.valid_all === '0' && result.valid_receive === '1'
+ console.log(`work_validate() output for bad max threshold nonce is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
- result = await NP.validate('29a9ae0236990e2e', '32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F', { debug: isDebug })
- console.log(`validate() output for slightly wrong nonce is ${result === false ? 'correct' : 'incorrect'}`)
- expect.push(result === false)
+ result = await NP.work_validate('29a9ae0236990e2e', '32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F', { debug: isDebug })
+ result = result.valid_all === '0'
+ console.log(`work_validate() output for slightly wrong nonce is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
- result = await NP.validate('7d903b18d03f9820', '39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150', { threshold: 0xfffffe00, debug: isDebug })
- console.log(`validate() output for bad receive threshold nonce is ${result === false ? 'correct' : 'incorrect'}`)
- expect.push(result === false)
+ result = await NP.work_validate('7d903b18d03f9820', '39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150', { threshold: 0xfffffe0000000000n, debug: isDebug })
+ result = result.valid === '0' && result.valid_all === '0' && result.valid_receive === '0'
+ console.log(`work_validate() output for bad receive threshold nonce is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
- result = await NP.validate('e45835c3b291c3d1', '9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F', { threshold: 0xffffffff, debug: isDebug })
- console.log(`validate() output for send threshold nonce that does not meet custom threshold is ${result === false ? 'correct' : 'incorrect'}`)
- expect.push(result === false)
+ result = await NP.work_validate('e45835c3b291c3d1', '9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F', { threshold: 0xffffffff00000000n, debug: isDebug })
+ result = result.valid === '0' && result.valid_all === '1' && result.valid_receive === '1'
+ console.log(`work_validate() output for send threshold nonce that does not meet custom threshold is ${result === true ? 'correct' : 'incorrect'}`)
+ expect.push(result)
try {
if (!expect.every(result => result)) throw new Error(`Validation is not working`)
} catch (err) {
+ document.getElementById('status').innerHTML = `FAILED TO VALIDATE KNOWN VALUES`
document.getElementById('output').innerHTML += `Error: ${err.message}<br/>`
console.error(err)
return
}
+ document.getElementById('status').innerHTML = `TESTING IN PROGRESS 0/${size}`
console.log(`%cNanoPow (${type})`, 'color:green', `Calculate proof-of-work for ${size} unique send block hashes`)
const times = []
for (let i = 0; i < size; i++) {
document.getElementById('status').innerHTML = `TESTING IN PROGRESS ${i}/${size}<br/>`
const hash = random()
- let work = null
+ let result = null
const start = performance.now()
try {
- work = await NP.search(hash, { threshold, effort, debug: isDebug })
+ result = await NP.work_generate(hash, { threshold, effort, debug: isDebug })
} catch (err) {
document.getElementById('output').innerHTML += `Error: ${err.message}<br/>`
console.error(err)
return
}
const end = performance.now()
- const isValid = (await NP.validate(work, hash, { threshold, debug: isDebug })) ? 'VALID' : 'INVALID'
+ const check = await NP.work_validate(result.work, result.hash, { threshold, debug: isDebug })
+ const isValid = (result.hash === hash && check.valid === '1') ? 'VALID' : 'INVALID'
times.push(end - start)
- const msg = `${isValid} [${work}] ${hash} (${end - start} ms)`
+ const msg = `${isValid} [${result.work}] ${result.hash} (${end - start} ms)`
if (isOutputShown) document.getElementById('output').innerHTML += `${msg}<br/>`
}
document.getElementById('output').innerHTML += `<hr/>`
validation.innerText = '⏳'
if (work.value.length === 16 && hash.value.length === 64) {
const NP = isGlForced ? NanoPowGl : NanoPow
- NP.validate(work.value, hash.value, { threshold: `0x${+threshold.value}` })
+ NP.work_validate(work.value, hash.value, { threshold: threshold.value })
.then(result => {
validation.innerText = result
? '✔️'
const isOutputShown = document.getElementById('isOutputShown')
const isGlForced = document.getElementById('isGlForced')
const isDebug = document.getElementById('isDebug')
- run(+`0x${threshold.value}`, +size.value, +effort.value, isOutputShown.checked, isGlForced.checked, isDebug.checked)
+ run(threshold.value, +size.value, +effort.value, isOutputShown.checked, isGlForced.checked, isDebug.checked)
}
document.getElementById('btnStartTest').addEventListener('click', startTest)
document.getElementById('effort').value = Math.max(1, Math.floor(navigator.hardwareConcurrency))
</script>
- <style>body{background:black;color:white;}a{color:darkcyan;}input[type=number]{width:5em;}span{margin:0.5em;}</style>
+ <style>
+ body{background:black;color:white;}a{color:darkcyan;}input[type=number]{width:5em;}span{margin:0.5em;}
+ label.hex::after{color:grey;content:'0x';display:inline-block;font-size:90%;left:0.5em;position:relative;width:0;}
+ label.hex+input{padding-left:1.25em;}
+ </style>
</head>
<body>
<p>Times below are in milliseconds and are summarized by various averaging methods.</p>
<p>Level of Effort depends on hardware and does not guarantee faster results.</p>
<hr />
- <label for="threshold">Threshold 0x</label>
- <input id="threshold" type="text" value="FFFFFFF8" />
+ <label for="threshold" class="hex">Threshold</label>
+ <input id="threshold" type="text" value="FFFFFFF800000000" />
<hr />
- <label for="work">Validate Work</label>
+ <label for="work" class="hex">Validate Work</label>
<input id="work" type="text" />
- <label for="hash">Hash</label>
+ <label for="hash" class="hex">Hash</label>
<input id="hash" type="text" />
<span id="validation"></span>
<hr />
--- /dev/null
+# SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+npm start
+sleep 2s
+
+printf '\nGet documentation\n'
+curl localhost:3000
+
+printf '\nExpect error. Server should not crash when bad data is received like missing end quote\n'
+curl -d '{ "action": "work_validate", "work": "47c83266398728cf", "hash: "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D }' localhost:3000
+
+printf '\nValidate good hashes\n'
+curl -d '{ "action": "work_validate", "work": "47c83266398728cf", "hash": "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D" }' localhost:3000
+curl -d '{ "action": "work_validate", "work": "4a8fb104eebbd336", "hash": "8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01" }' localhost:3000
+curl -d '{ "action": "work_validate", "work": "326f310d629a8a98", "hash": "204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA", "difficulty": "ffffffff00000000" }' localhost:3000
+curl -d '{ "action": "work_validate", "work": "c5d5d6f7c5d6ccd1", "hash": "281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117" }' localhost:3000
+curl -d '{ "action": "work_validate", "work": "6866c1ac3831a891", "hash": "7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090", "difficulty": "fffffe0000000000" }' localhost:3000
+
+printf '\nValidate bad hashes\n'
+curl -d '{ "action": "work_validate", "work": "0000000000000000", "hash": "0000000000000000000000000000000000000000000000000000000000000000" }' localhost:3000
+curl -d '{ "action": "work_validate", "work": "c5d5d6f7c5d6ccd1", "hash": "BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E" }' localhost:3000
+curl -d '{ "action": "work_validate", "work": "ae238556213c3624", "hash": "BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6", "difficulty": "ffffffff00000000" }' localhost:3000
+curl -d '{ "action": "work_validate", "work": "29a9ae0236990e2e", "hash": "32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F" }' localhost:3000
+curl -d '{ "action": "work_validate", "work": "7d903b18d03f9820", "hash": "39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150", "difficulty": "fffffe0000000000" }' localhost:3000
+curl -d '{ "action": "work_validate", "work": "e45835c3b291c3d1", "hash": "9DCD89E2B92FD59D7358C2C2E4C225DF94C88E187B27882F50FEFC3760D3994F", "difficulty": "ffffffff00000000" }' localhost:3000
+
+
+printf '\nGenerate\n'
+curl -d '{ "action": "work_generate", "hash": "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D" }' localhost:3000
+curl -d '{ "action": "work_generate", "hash": "204076E3364D16A018754FF67D418AB2FBEB38799FF9A29A1D5F9E34F16BEEEA", "difficulty": "ffffffff00000000" }' localhost:3000
+curl -d '{ "action": "work_generate", "hash": "7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090", "difficulty": "fffffe0000000000" }' localhost:3000
+kill $(cat server.pid) && rm server.pid