]> zoso.dev Git - nano-pow.git/commitdiff
Add validate function to check passed nonces.
authorChris Duncan <chris@zoso.dev>
Sun, 12 Jan 2025 06:57:56 +0000 (22:57 -0800)
committerChris Duncan <chris@zoso.dev>
Sun, 12 Jan 2025 06:57:56 +0000 (22:57 -0800)
src/classes/gpu.ts
test.html

index 7b825f3942de6e7b43c591679d4dbd18b293ac0a..84981d429ca8df92c15d69d45ba74260c1ab6ae4 100644 (file)
@@ -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<boolean> {
+               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
+       }
 }
index a93c46ffc29569fbac00660824a6f3988604d009..3f6bf6f0dd162d30531ef1e36db9c1c1e6fdcaf6 100644 (file)
--- 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}<br/>`
+                               document.getElementById('output').innerHTML += `Error: ${err.message}<br/>`
+                               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}<br/>`
                }