From 5214fc7b3de21e84c1bf047b3d23a99787442441 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Sat, 11 Jan 2025 22:57:56 -0800 Subject: [PATCH] Add validate function to check passed nonces. --- src/classes/gpu.ts | 113 +++++++++++++++++++++++++++++++++++++++++---- test.html | 8 ++-- 2 files changed, 108 insertions(+), 13 deletions(-) diff --git a/src/classes/gpu.ts b/src/classes/gpu.ts index 7b825f3..84981d4 100644 --- a/src/classes/gpu.ts +++ b/src/classes/gpu.ts @@ -27,18 +27,12 @@ export class NanoPowGpu { if (this.#busy) return this.#busy = true // Request device and adapter - if (navigator.gpu == null) { - throw new Error('WebGPU is not supported in this browser.') - } + if (navigator.gpu == null) throw new Error('WebGPU is not supported in this browser.') try { const adapter = await navigator.gpu.requestAdapter() - if (adapter == null) { - throw new Error('WebGPU adapter refused by browser.') - } + if (adapter == null) throw new Error('WebGPU adapter refused by browser.') const device = await adapter.requestDevice() - if (!(device instanceof GPUDevice)) { - throw new Error('WebGPU device failed to load.') - } + if (!(device instanceof GPUDevice)) throw new Error('WebGPU device failed to load.') device.lost.then(this.reset) this.#device = device this.setup() @@ -139,7 +133,7 @@ export class NanoPowGpu { const uint32 = hash.slice(i, i + 8) uboView.setUint32(i / 2, parseInt(uint32, 16)) } - const random = Math.floor((Math.random() * 0xffffffff)) + const random = Math.floor(Math.random() * 0xffffffff) uboView.setUint32(32, random, true) uboView.setUint32(36, random, true) uboView.setUint32(40, threshold, true) @@ -200,4 +194,103 @@ export class NanoPowGpu { } while (this.#busy) return nonce.toString(16).padStart(16, '0') } + + /** + * Validates that a nonce satisfies Nano proof-of-work requirements. + * + * @param {string} work - Hexadecimal proof-of-work value + * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts + * @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation + */ + static async validate (work: string, hash: string, threshold: number = 0xfffffff8): Promise { + if (this.#busy) { + return new Promise(resolve => { + setTimeout(async () => { + const result = this.validate(work, hash, threshold) + resolve(result) + }, 100) + }) + } + this.#busy = true + 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 (typeof threshold !== 'number') throw new TypeError(`Invalid threshold ${threshold}`) + + // 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) 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 + const uboView = new DataView(new ArrayBuffer(48)) + for (let i = 0; i < 64; i += 8) { + const uint32 = hash.slice(i, i + 8) + uboView.setUint32(i / 2, parseInt(uint32, 16)) + } + uboView.setBigUint64(32, BigInt(`0x${work}`), true) + uboView.setUint32(40, threshold, true) + this.#device.queue.writeBuffer(this.#uboBuffer, 0, uboView) + + // Reset `nonce` and `found` to 0u in WORK before each calculation + this.#device.queue.writeBuffer(this.#gpuBuffer, 0, new Uint32Array([0, 0, 0])) + + // Bind UBO read and GPU write buffers + const bindGroup = this.#device.createBindGroup({ + layout: this.#bindGroupLayout, + entries: [ + { + binding: 0, + resource: { + buffer: this.#uboBuffer + }, + }, + { + binding: 1, + resource: { + buffer: this.#gpuBuffer + }, + }, + ], + }) + + // 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(this.#pipeline) + passEncoder.setBindGroup(0, bindGroup) + passEncoder.dispatchWorkgroups(1) + passEncoder.end() + + // Copy 8-byte nonce and 4-byte found flag from GPU to CPU for reading + commandEncoder.copyBufferToBuffer(this.#gpuBuffer, 0, this.#cpuBuffer, 0, 12) + + // End computation by passing array of command buffers to command queue for execution + this.#device.queue.submit([commandEncoder.finish()]) + + // Read results back to Javascript and then unmap buffer after reading + let data = null + try { + await this.#cpuBuffer.mapAsync(GPUMapMode.READ) + await this.#device.queue.onSubmittedWorkDone() + data = new DataView(this.#cpuBuffer.getMappedRange().slice(0)) + this.#cpuBuffer.unmap() + } catch (err) { + console.warn(`Error getting data from GPU. ${err}`) + return this.validate(work, hash, threshold) + } + if (data == null) throw new Error(`Failed to get data from buffer.`) + + const nonce = data.getBigUint64(0, true).toString(16).padStart(16, '0') + const found = !!data.getUint32(8) + this.#busy = false + if (found && work !== nonce) throw new Error(`Nonce found but does not match work`) + return found + } } diff --git a/test.html b/test.html index a93c46f..3f6bf6f 100644 --- a/test.html +++ b/test.html @@ -58,21 +58,23 @@ SPDX-License-Identifier: GPL-3.0-or-later console.log(`Geometric Mean: ${geometric} ms`) } - console.log(`%cNanoPowGpu `, 'color:green', `Calculate proof-of-work for ${COUNT} unique send block hashes`) times = [] for (let i = 0; i < COUNT; i++) { const hash = random() let work = null + let isValid = null const start = performance.now() try { work = await NanoPowGpu.search(hash) + isValid = (await NanoPowGpu.validate(work, hash)) ? 'VALID' : 'INVALID' } catch (err) { - document.getElementById('output').innerHTML += `${err.message}
` + document.getElementById('output').innerHTML += `Error: ${err.message}
` + console.error(err) } const end = performance.now() times.push(end - start) - const msg = `[${work}] ${hash} (${end - start} ms)` + const msg = `${isValid} [${work}] ${hash} (${end - start} ms)` console.log(msg) document.getElementById('output').innerHTML += `${msg}
` } -- 2.34.1