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()
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)
} 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
+ }
}