UNPKG

nano-pow

Version:

Proof-of-work generation and validation with WebGPU/WebGL for Nano cryptocurrency.

817 lines (808 loc) 47.6 kB
// src/lib/gl/gl-downsample.frag var gl_downsample_default = `#version 300 es #pragma vscode_glsllint_stage: frag //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later #ifdef GL_FRAGMENT_PRECISION_HIGH precision highp float; #else precision mediump float; #endif precision highp int;out uvec4 nonce;uniform highp usampler2D src;void main(){nonce=uvec4(0u);vec2 inputSize=vec2(textureSize(src,0));vec2 texel=vec2(1.0)/inputSize;vec2 blockCoord=(floor(gl_FragCoord.xy)*2.0+vec2(0.5))/inputSize;uvec4 pixel=texture(src,blockCoord);nonce=pixel.x==0u?nonce:pixel;pixel=texture(src,blockCoord+vec2(texel.x,0.0));nonce=pixel.x==0u?nonce:pixel;pixel=texture(src,blockCoord+vec2(0.0,texel.y));nonce=pixel.x==0u?nonce:pixel;pixel=texture(src,blockCoord+vec2(texel.x,texel.y));nonce=pixel.x==0u?nonce:pixel;}`; // src/lib/gl/gl-draw.frag var gl_draw_default = `#version 300 es #pragma vscode_glsllint_stage: frag //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-FileContributor: Ben Green <ben@latenightsketches.com> //! SPDX-License-Identifier: GPL-3.0-or-later AND MIT #ifdef GL_FRAGMENT_PRECISION_HIGH precision highp float; #else precision mediump float; #endif out uvec4 nonce;layout(std140)uniform UBO{uint blockhash[8];uvec2 difficulty;bool validate;};layout(std140)uniform WORK{uvec2 seed;};const uvec2 BLAKE2B_IV[8]=uvec2[8](uvec2(0xF3BCC908u,0x6A09E667u),uvec2(0x84CAA73Bu,0xBB67AE85u),uvec2(0xFE94F82Bu,0x3C6EF372u),uvec2(0x5F1D36F1u,0xA54FF53Au),uvec2(0xADE682D1u,0x510E527Fu),uvec2(0x2B3E6C1Fu,0x9B05688Cu),uvec2(0xFB41BD6Bu,0x1F83D9ABu),uvec2(0x137E2179u,0x5BE0CD19u));const uvec2 BLAKE2B_PARAM=uvec2(0x01010008u,0u);const uvec2 BLAKE2B_INLEN=uvec2(0x00000028u,0u);const uvec2 BLAKE2B_FINAL=uvec2(0xFFFFFFFFu,0xFFFFFFFFu);const uvec2 BLAKE2B_INIT[16]=uvec2[16](BLAKE2B_IV[0u]^ BLAKE2B_PARAM,BLAKE2B_IV[1u],BLAKE2B_IV[2u],BLAKE2B_IV[3u],BLAKE2B_IV[4u],BLAKE2B_IV[5u],BLAKE2B_IV[6u],BLAKE2B_IV[7u],BLAKE2B_IV[0u],BLAKE2B_IV[1u],BLAKE2B_IV[2u],BLAKE2B_IV[3u],BLAKE2B_IV[4u]^ BLAKE2B_INLEN,BLAKE2B_IV[5u],BLAKE2B_IV[6u]^ BLAKE2B_FINAL,BLAKE2B_IV[7u]);const uint BLAKE2B_IV32_1=0x6A09E667u;const uvec4 ROTATE_1=uvec4(1u);const uvec4 ROTATE_8=uvec4(8u);const uvec4 ROTATE_16=uvec4(16u);const uvec4 ROTATE_24=uvec4(24u);const uvec4 ROTATE_31=uvec4(31u);uvec2 v[16];uvec2 m[16];void G(uint a0,uint b0,uint c0,uint d0,uvec2 x0,uvec2 y0,uint a1,uint b1,uint c1,uint d1,uvec2 x1,uvec2 y1){uvec4 a=uvec4(v[a0],v[a1]);uvec4 b=uvec4(v[b0],v[b1]);uvec4 c=uvec4(v[c0],v[c1]);uvec4 d=uvec4(v[d0],v[d1]);uvec4 mx=uvec4(x0,x1);uvec4 my=uvec4(y0,y1);a=a+b+uvec4(0u,uint(a.x+b.x<a.x),0u,uint(a.z+b.z<a.z));a=a+mx+uvec4(0u,uint(a.x+mx.x<a.x),0u,uint(a.z+mx.z<a.z));d=(d ^ a).yxwz;c=c+d+uvec4(0u,uint(c.x+d.x<c.x),0u,uint(c.z+d.z<c.z));b=((b ^ c)>>ROTATE_24)|((b ^ c)<<ROTATE_8).yxwz;a=a+b+uvec4(0u,uint(a.x+b.x<b.x),0u,uint(a.z+b.z<b.z));a=a+my+uvec4(0u,uint(a.x+my.x<a.x),0u,uint(a.z+my.z<a.z));d=((d ^ a)>>ROTATE_16)|((d ^ a)<<ROTATE_16).yxwz;c=c+d+uvec4(0u,uint(c.x+d.x<c.x),0u,uint(c.z+d.z<c.z));b=((b ^ c)>>ROTATE_31).yxwz|((b ^ c)<<ROTATE_1);v[a0]=a.xy;v[b0]=b.xy;v[c0]=c.xy;v[d0]=d.xy;v[a1]=a.zw;v[b1]=b.zw;v[c1]=c.zw;v[d1]=d.zw;}void main(){nonce=uvec4(0u);m[0u]=seed ^ uvec2(gl_FragCoord);m[1u]=uvec2(blockhash[0u],blockhash[1u]);m[2u]=uvec2(blockhash[2u],blockhash[3u]);m[3u]=uvec2(blockhash[4u],blockhash[5u]);m[4u]=uvec2(blockhash[6u],blockhash[7u]);v=BLAKE2B_INIT;G(0u,4u,8u,12u,m[0u],m[1u],1u,5u,9u,13u,m[2u],m[3u]);G(2u,6u,10u,14u,m[4u],m[5u],3u,7u,11u,15u,m[6u],m[7u]);G(0u,5u,10u,15u,m[8u],m[9u],1u,6u,11u,12u,m[10u],m[11u]);G(2u,7u,8u,13u,m[12u],m[13u],3u,4u,9u,14u,m[14u],m[15u]);G(0u,4u,8u,12u,m[14u],m[10u],1u,5u,9u,13u,m[4u],m[8u]);G(2u,6u,10u,14u,m[9u],m[15u],3u,7u,11u,15u,m[13u],m[6u]);G(0u,5u,10u,15u,m[1u],m[12u],1u,6u,11u,12u,m[0u],m[2u]);G(2u,7u,8u,13u,m[11u],m[7u],3u,4u,9u,14u,m[5u],m[3u]);G(0u,4u,8u,12u,m[11u],m[8u],1u,5u,9u,13u,m[12u],m[0u]);G(2u,6u,10u,14u,m[5u],m[2u],3u,7u,11u,15u,m[15u],m[13u]);G(0u,5u,10u,15u,m[10u],m[14u],1u,6u,11u,12u,m[3u],m[6u]);G(2u,7u,8u,13u,m[7u],m[1u],3u,4u,9u,14u,m[9u],m[4u]);G(0u,4u,8u,12u,m[7u],m[9u],1u,5u,9u,13u,m[3u],m[1u]);G(2u,6u,10u,14u,m[13u],m[12u],3u,7u,11u,15u,m[11u],m[14u]);G(0u,5u,10u,15u,m[2u],m[6u],1u,6u,11u,12u,m[5u],m[10u]);G(2u,7u,8u,13u,m[4u],m[0u],3u,4u,9u,14u,m[15u],m[8u]);G(0u,4u,8u,12u,m[9u],m[0u],1u,5u,9u,13u,m[5u],m[7u]);G(2u,6u,10u,14u,m[2u],m[4u],3u,7u,11u,15u,m[10u],m[15u]);G(0u,5u,10u,15u,m[14u],m[1u],1u,6u,11u,12u,m[11u],m[12u]);G(2u,7u,8u,13u,m[6u],m[8u],3u,4u,9u,14u,m[3u],m[13u]);G(0u,4u,8u,12u,m[2u],m[12u],1u,5u,9u,13u,m[6u],m[10u]);G(2u,6u,10u,14u,m[0u],m[11u],3u,7u,11u,15u,m[8u],m[3u]);G(0u,5u,10u,15u,m[4u],m[13u],1u,6u,11u,12u,m[7u],m[5u]);G(2u,7u,8u,13u,m[15u],m[14u],3u,4u,9u,14u,m[1u],m[9u]);G(0u,4u,8u,12u,m[12u],m[5u],1u,5u,9u,13u,m[1u],m[15u]);G(2u,6u,10u,14u,m[14u],m[13u],3u,7u,11u,15u,m[4u],m[10u]);G(0u,5u,10u,15u,m[0u],m[7u],1u,6u,11u,12u,m[6u],m[3u]);G(2u,7u,8u,13u,m[9u],m[2u],3u,4u,9u,14u,m[8u],m[11u]);G(0u,4u,8u,12u,m[13u],m[11u],1u,5u,9u,13u,m[7u],m[14u]);G(2u,6u,10u,14u,m[12u],m[1u],3u,7u,11u,15u,m[3u],m[9u]);G(0u,5u,10u,15u,m[5u],m[0u],1u,6u,11u,12u,m[15u],m[4u]);G(2u,7u,8u,13u,m[8u],m[6u],3u,4u,9u,14u,m[2u],m[10u]);G(0u,4u,8u,12u,m[6u],m[15u],1u,5u,9u,13u,m[14u],m[9u]);G(2u,6u,10u,14u,m[11u],m[3u],3u,7u,11u,15u,m[0u],m[8u]);G(0u,5u,10u,15u,m[12u],m[2u],1u,6u,11u,12u,m[13u],m[7u]);G(2u,7u,8u,13u,m[1u],m[4u],3u,4u,9u,14u,m[10u],m[5u]);G(0u,4u,8u,12u,m[10u],m[2u],1u,5u,9u,13u,m[8u],m[4u]);G(2u,6u,10u,14u,m[7u],m[6u],3u,7u,11u,15u,m[1u],m[5u]);G(0u,5u,10u,15u,m[15u],m[11u],1u,6u,11u,12u,m[9u],m[14u]);G(2u,7u,8u,13u,m[3u],m[12u],3u,4u,9u,14u,m[13u],m[0u]);G(0u,4u,8u,12u,m[0u],m[1u],1u,5u,9u,13u,m[2u],m[3u]);G(2u,6u,10u,14u,m[4u],m[5u],3u,7u,11u,15u,m[6u],m[7u]);G(0u,5u,10u,15u,m[8u],m[9u],1u,6u,11u,12u,m[10u],m[11u]);G(2u,7u,8u,13u,m[12u],m[13u],3u,4u,9u,14u,m[14u],m[15u]);G(0u,4u,8u,12u,m[14u],m[10u],1u,5u,9u,13u,m[4u],m[8u]);G(2u,6u,10u,14u,m[9u],m[15u],3u,7u,11u,15u,m[13u],m[6u]);G(0u,5u,10u,15u,m[1u],m[12u],1u,6u,11u,12u,m[0u],m[2u]);G(2u,7u,8u,13u,m[11u],m[7u],3u,4u,9u,14u,m[5u],m[3u]);uvec2 result=BLAKE2B_INIT[0u]^ v[0u]^ v[8u];if((validate&&uvec2(gl_FragCoord)==uvec2(0u))||(result.y>difficulty.y||(result.y==difficulty.y&&result.x>difficulty.x))){nonce=uvec4((BLAKE2B_INIT[0u]^ v[0u]^ v[8u]),m[0u]).yxwz;}if(nonce.x==0u){discard;}}`; // src/lib/gl/gl-vertex.vert var gl_vertex_default = `#version 300 es #pragma vscode_glsllint_stage: vert //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-FileContributor: Ben Green <ben@latenightsketches.com> //! SPDX-License-Identifier: GPL-3.0-or-later AND MIT #ifdef GL_FRAGMENT_PRECISION_HIGH precision highp float; #else precision mediump float; #endif layout(location=0)in vec4 position;void main(){gl_Position=position;}`; // src/utils/index.ts function isHex(input, min, max) { 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 === void 0 && max === void 0 ? "+" : `{${min ?? "0"},${max ?? ""}}`; const regexp = new RegExp(`^[0-9A-Fa-f]${range}$`, "m"); return regexp.test(input); } function isNotHex(input, min, max) { return !isHex(input, min, max); } // src/lib/gl/index.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-FileContributor: Ben Green <ben@latenightsketches.com> //! SPDX-License-Identifier: GPL-3.0-or-later AND MIT var NanoPowGl = class _NanoPowGl { static #SEND = 0xfffffff800000000n; static #RECEIVE = 0xfffffe0000000000n; static #isInitialized = false; static #busy = false; static #debug = false; static #raf = 0; /** Used to set canvas size. */ static #cores = Math.max(1, Math.floor(navigator.hardwareConcurrency)); static #WORKLOAD = 256 * this.#cores; static #canvas; static #gl; static #drawProgram; static #downsampleProgram; static #vertexShader; static #drawShader; static #downsampleShader; static #positionBuffer; static #drawFbo; static #downsampleFbos = []; static #downsampleSrcLocation; static #uboBuffer; static #uboView = new DataView(new ArrayBuffer(144)); static #seedBuffer; static #seed = new BigUint64Array(1); static #query; static #pixels; /** Vertex positions for fullscreen quad. */ static #positions = new Float32Array([ -1, -1, 1, -1, 1, 1, -1, 1 ]); /** * Constructs canvas, gets WebGL context, initializes buffers, and compiles * shaders. */ static async init() { if (this.#busy) return; this.#busy = true; try { this.#canvas = new OffscreenCanvas(this.#WORKLOAD, this.#WORKLOAD); this.#canvas.addEventListener("webglcontextlost", (event) => { event.preventDefault(); console.warn("WebGL context lost. Waiting for it to be restored..."); cancelAnimationFrame(this.#raf); }, false); this.#canvas.addEventListener("webglcontextrestored", (event) => { console.warn("WebGL context restored. Reinitializing..."); _NanoPowGl.init(); }, false); this.#gl = this.#canvas.getContext("webgl2"); if (this.#gl == null) throw new Error("WebGL 2 is required"); this.#drawProgram = this.#gl.createProgram(); if (this.#drawProgram == null) throw new Error("Failed to create shader program"); this.#vertexShader = this.#gl.createShader(this.#gl.VERTEX_SHADER); if (this.#vertexShader == null) throw new Error("Failed to create vertex shader"); this.#gl.shaderSource(this.#vertexShader, gl_vertex_default); this.#gl.compileShader(this.#vertexShader); if (!this.#gl.getShaderParameter(this.#vertexShader, this.#gl.COMPILE_STATUS)) throw new Error(this.#gl.getShaderInfoLog(this.#vertexShader) ?? `Failed to compile vertex shader`); this.#drawShader = this.#gl.createShader(this.#gl.FRAGMENT_SHADER); if (this.#drawShader == null) throw new Error("Failed to create fragment shader"); this.#gl.shaderSource(this.#drawShader, gl_draw_default); this.#gl.compileShader(this.#drawShader); if (!this.#gl.getShaderParameter(this.#drawShader, this.#gl.COMPILE_STATUS)) throw new Error(this.#gl.getShaderInfoLog(this.#drawShader) ?? `Failed to compile fragment shader`); this.#gl.attachShader(this.#drawProgram, this.#vertexShader); this.#gl.attachShader(this.#drawProgram, this.#drawShader); this.#gl.linkProgram(this.#drawProgram); if (!this.#gl.getProgramParameter(this.#drawProgram, this.#gl.LINK_STATUS)) throw new Error(this.#gl.getProgramInfoLog(this.#drawProgram) ?? `Failed to link program`); this.#downsampleProgram = this.#gl.createProgram(); if (this.#downsampleProgram == null) throw new Error("Failed to create downsample program"); this.#downsampleShader = this.#gl.createShader(this.#gl.FRAGMENT_SHADER); if (this.#downsampleShader == null) throw new Error("Failed to create downsample shader"); this.#gl.shaderSource(this.#downsampleShader, gl_downsample_default); this.#gl.compileShader(this.#downsampleShader); if (!this.#gl.getShaderParameter(this.#downsampleShader, this.#gl.COMPILE_STATUS)) throw new Error(this.#gl.getShaderInfoLog(this.#downsampleShader) ?? `Failed to compile downsample shader`); this.#gl.attachShader(this.#downsampleProgram, this.#vertexShader); this.#gl.attachShader(this.#downsampleProgram, this.#downsampleShader); this.#gl.linkProgram(this.#downsampleProgram); if (!this.#gl.getProgramParameter(this.#downsampleProgram, this.#gl.LINK_STATUS)) throw new Error(this.#gl.getProgramInfoLog(this.#downsampleProgram) ?? `Failed to link program`); this.#gl.useProgram(this.#drawProgram); const triangleArray = this.#gl.createVertexArray(); this.#gl.bindVertexArray(triangleArray); this.#positionBuffer = this.#gl.createBuffer(); this.#gl.bindBuffer(this.#gl.ARRAY_BUFFER, this.#positionBuffer); this.#gl.bufferData(this.#gl.ARRAY_BUFFER, this.#positions, this.#gl.STATIC_DRAW); this.#gl.vertexAttribPointer(0, 2, this.#gl.FLOAT, false, 0, 0); this.#gl.enableVertexAttribArray(0); this.#gl.bindBuffer(this.#gl.ARRAY_BUFFER, null); const texture = this.#gl.createTexture(); this.#gl.bindTexture(this.#gl.TEXTURE_2D, texture); this.#gl.texImage2D(this.#gl.TEXTURE_2D, 0, this.#gl.RGBA32UI, this.#gl.drawingBufferWidth, this.#gl.drawingBufferHeight, 0, this.#gl.RGBA_INTEGER, this.#gl.UNSIGNED_INT, null); this.#gl.texParameteri(this.#gl.TEXTURE_2D, this.#gl.TEXTURE_MIN_FILTER, this.#gl.NEAREST); this.#gl.texParameteri(this.#gl.TEXTURE_2D, this.#gl.TEXTURE_MAG_FILTER, this.#gl.NEAREST); const framebuffer = this.#gl.createFramebuffer(); this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, framebuffer); this.#gl.framebufferTexture2D(this.#gl.FRAMEBUFFER, this.#gl.COLOR_ATTACHMENT0, this.#gl.TEXTURE_2D, texture, 0); if (this.#gl.checkFramebufferStatus(this.#gl.FRAMEBUFFER) !== this.#gl.FRAMEBUFFER_COMPLETE) throw new Error(`Failed to create drawing framebuffer`); this.#drawFbo = { texture, framebuffer, size: { x: this.#gl.drawingBufferWidth, y: this.#gl.drawingBufferHeight } }; for (let i = 1; i <= 4; i++) { const width = this.#gl.drawingBufferWidth / 2 ** i; const height = this.#gl.drawingBufferHeight / 2 ** i; const texture2 = this.#gl.createTexture(); this.#gl.bindTexture(this.#gl.TEXTURE_2D, texture2); this.#gl.texImage2D(this.#gl.TEXTURE_2D, 0, this.#gl.RGBA32UI, width, height, 0, this.#gl.RGBA_INTEGER, this.#gl.UNSIGNED_INT, null); this.#gl.texParameteri(this.#gl.TEXTURE_2D, this.#gl.TEXTURE_MIN_FILTER, this.#gl.NEAREST); this.#gl.texParameteri(this.#gl.TEXTURE_2D, this.#gl.TEXTURE_MAG_FILTER, this.#gl.NEAREST); const framebuffer2 = this.#gl.createFramebuffer(); this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, framebuffer2); this.#gl.framebufferTexture2D(this.#gl.FRAMEBUFFER, this.#gl.COLOR_ATTACHMENT0, this.#gl.TEXTURE_2D, texture2, 0); if (this.#gl.checkFramebufferStatus(this.#gl.FRAMEBUFFER) !== this.#gl.FRAMEBUFFER_COMPLETE) throw new Error(`Failed to create downsampling framebuffer ${i}`); this.#downsampleFbos.push({ texture: texture2, framebuffer: framebuffer2, size: { x: width, y: height } }); } this.#downsampleSrcLocation = this.#gl.getUniformLocation(this.#downsampleProgram, "src"); this.#gl.bindTexture(this.#gl.TEXTURE_2D, null); this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, null); this.#uboBuffer = this.#gl.createBuffer(); this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#uboBuffer); this.#gl.bufferData(this.#gl.UNIFORM_BUFFER, 144, this.#gl.DYNAMIC_DRAW); this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null); this.#gl.bindBufferBase(this.#gl.UNIFORM_BUFFER, 0, this.#uboBuffer); this.#gl.uniformBlockBinding(this.#drawProgram, this.#gl.getUniformBlockIndex(this.#drawProgram, "UBO"), 0); this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null); this.#seedBuffer = this.#gl.createBuffer(); this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#seedBuffer); this.#gl.bufferData(this.#gl.UNIFORM_BUFFER, 16, this.#gl.DYNAMIC_DRAW); this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null); this.#gl.bindBufferBase(this.#gl.UNIFORM_BUFFER, 1, this.#seedBuffer); this.#gl.uniformBlockBinding(this.#drawProgram, this.#gl.getUniformBlockIndex(this.#drawProgram, "WORK"), 1); this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null); 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; } } /** * On WebGL context loss, attempts to clear all program variables and then * reinitialize them by calling `init()`. */ static reset() { cancelAnimationFrame(_NanoPowGl.#raf); _NanoPowGl.#gl?.deleteQuery(_NanoPowGl.#query); _NanoPowGl.#query = null; _NanoPowGl.#gl?.deleteBuffer(_NanoPowGl.#seedBuffer); _NanoPowGl.#seedBuffer = null; _NanoPowGl.#gl?.deleteBuffer(_NanoPowGl.#uboBuffer); _NanoPowGl.#uboBuffer = null; for (const fbo of _NanoPowGl.#downsampleFbos) { _NanoPowGl.#gl?.deleteFramebuffer(fbo.framebuffer); _NanoPowGl.#gl?.deleteTexture(fbo.texture); } _NanoPowGl.#downsampleFbos = []; _NanoPowGl.#gl?.deleteShader(_NanoPowGl.#downsampleShader); _NanoPowGl.#downsampleShader = null; _NanoPowGl.#gl?.deleteProgram(_NanoPowGl.#downsampleProgram); _NanoPowGl.#downsampleProgram = null; _NanoPowGl.#gl?.deleteFramebuffer(_NanoPowGl.#drawFbo?.framebuffer ?? null); _NanoPowGl.#drawFbo = null; _NanoPowGl.#gl?.deleteTexture(_NanoPowGl.#drawFbo); _NanoPowGl.#drawFbo = null; _NanoPowGl.#gl?.deleteBuffer(_NanoPowGl.#positionBuffer); _NanoPowGl.#positionBuffer = null; _NanoPowGl.#gl?.deleteShader(_NanoPowGl.#drawShader); _NanoPowGl.#drawShader = null; _NanoPowGl.#gl?.deleteShader(_NanoPowGl.#vertexShader); _NanoPowGl.#vertexShader = null; _NanoPowGl.#gl?.deleteProgram(_NanoPowGl.#drawProgram); _NanoPowGl.#drawProgram = null; _NanoPowGl.#gl = null; _NanoPowGl.#busy = false; _NanoPowGl.#isInitialized = false; _NanoPowGl.init(); } static #logAverages(times) { let count = times.length, sum = 0, reciprocals = 0, logarithms = 0, truncated = 0, min = 65535, max = 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 (count < 3 || i > count * 0.1 && i < count * 0.9) truncated += times[i]; } const averages = { "Count (frames)": count, "Total (ms)": sum, "Rate (f/s)": 1e3 * count * 0.8 / (truncated || sum), "Minimum (ms)": min, "Maximum (ms)": max, "Arithmetic Mean (ms)": sum / count, "Truncated Mean (ms)": truncated / count, "Harmonic Mean (ms)": count / reciprocals, "Geometric Mean (ms)": Math.exp(logarithms / count) }; console.log(`Averages: ${JSON.stringify(averages)}`); console.table(averages); } static #draw(seed) { if (this.#gl == null || this.#query == null) throw new Error("WebGL 2 is required to draw and query pixels"); if (this.#drawFbo == null) throw new Error("FBO is required to draw"); if (this.#seed[0] == null || this.#seedBuffer == null) throw new Error("Seed is required to draw"); this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#seedBuffer); this.#gl.bufferSubData(this.#gl.UNIFORM_BUFFER, 0, seed); this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null); this.#gl.useProgram(this.#drawProgram); this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, this.#drawFbo.framebuffer); this.#gl.activeTexture(this.#gl.TEXTURE0); this.#gl.bindTexture(this.#gl.TEXTURE_2D, this.#drawFbo.texture); this.#gl.beginQuery(this.#gl.ANY_SAMPLES_PASSED_CONSERVATIVE, this.#query); this.#gl.viewport(0, 0, this.#drawFbo.size.x, this.#drawFbo.size.y); this.#gl.drawArrays(this.#gl.TRIANGLES, 0, 4); this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, null); this.#gl.endQuery(this.#gl.ANY_SAMPLES_PASSED_CONSERVATIVE); } static async #checkQueryResult() { return new Promise((resolve, reject) => { function check() { try { if (_NanoPowGl.#gl == null || _NanoPowGl.#query == null) throw new Error("WebGL 2 is required to check query results"); if (_NanoPowGl.#gl.getQueryParameter(_NanoPowGl.#query, _NanoPowGl.#gl.QUERY_RESULT_AVAILABLE)) { resolve(!!_NanoPowGl.#gl.getQueryParameter(_NanoPowGl.#query, _NanoPowGl.#gl.QUERY_RESULT)); } else { _NanoPowGl.#raf = requestAnimationFrame(check); } } catch (err) { reject(err); } } check(); }); } /** * When a result is found by the `gl.query`, downsamples the texture to speed * up the subsequent `readPixels` call, reads the pixels into the work buffer, * checks every 4th pixel for the 'found' byte, converts the subsequent two * pixels with the nonce byte values to a hex string, and returns the result. * * @param {string} [workHex] - Original nonce if provided for a validation call * @returns Nonce as an 8-byte (16-char) hexadecimal string */ static #readResult(workHex) { if (this.#gl == null) throw new Error("WebGL 2 is required to read pixels"); if (this.#drawFbo == null) throw new Error("Source FBO is required to downsample"); let source = this.#drawFbo; let pixelCount; const start = performance.now(); if (workHex != null) { this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, source.framebuffer); this.#gl.readPixels(0, 0, 1, 1, this.#gl.RGBA_INTEGER, this.#gl.UNSIGNED_INT, this.#pixels); pixelCount = 4; } else { this.#gl.useProgram(this.#downsampleProgram); for (const fbo of this.#downsampleFbos) { this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, fbo.framebuffer); this.#gl.activeTexture(this.#gl.TEXTURE0); this.#gl.bindTexture(this.#gl.TEXTURE_2D, source.texture); this.#gl.uniform1i(this.#downsampleSrcLocation, 0); this.#gl.viewport(0, 0, fbo.size.x, fbo.size.y); this.#gl.drawArrays(this.#gl.TRIANGLES, 0, 4); source = fbo; } this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, source.framebuffer); this.#gl.readPixels(0, 0, source.size.x, source.size.y, this.#gl.RGBA_INTEGER, this.#gl.UNSIGNED_INT, this.#pixels); pixelCount = source.size.x * source.size.y * 4; } this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, null); for (let i = 0; i < pixelCount; i += 4) { if (this.#pixels[i] !== 0) { if (this.#debug) console.log(`readResults (${performance.now() - start} ms)`); if (this.#debug) console.log(`Pixel: rgba(${this.#pixels[i]}, ${this.#pixels[i + 1]}, ${this.#pixels[i + 2]}, ${this.#pixels[i + 3]})`); const result = `${this.#pixels[i].toString(16).padStart(8, "0")}${this.#pixels[i + 1].toString(16).padStart(8, "0")}`; const nonce = `${this.#pixels[i + 2].toString(16).padStart(8, "0")}${this.#pixels[i + 3].toString(16).padStart(8, "0")}`; if (workHex == null || workHex == nonce) { return { result: BigInt(`0x${result}`), nonce: BigInt(`0x${nonce}`) }; } } } 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 - Options used to configure search execution */ static async work_generate(hash, options) { if (isNotHex(hash, 64)) throw new Error(`Invalid hash ${hash}`); if (this.#busy) { console.log("NanoPowGl is busy. Retrying search..."); return new Promise((resolve) => { setTimeout(async () => { const result2 = this.work_generate(hash, options); resolve(result2); }, 500); }); } if (this.#isInitialized === false) this.init(); options ??= {}; options.debug ??= false; options.difficulty ??= 0xfffffff800000000n; options.effort ??= 4; if (typeof options?.difficulty !== "string" && typeof options?.difficulty !== "bigint") { throw new TypeError(`Invalid difficulty ${options?.difficulty}`); } 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 < 1 || options.effort > 32 ? 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)); if (this.#WORKLOAD !== 256 * effort) { this.#WORKLOAD = 256 * effort; this.#canvas.height = this.#WORKLOAD; this.#canvas.width = this.#WORKLOAD; this.reset(); } if (_NanoPowGl.#gl == null) throw new Error("WebGL 2 is required"); if (this.#gl == null) throw new Error("WebGL 2 is required"); if (this.#drawFbo == null) throw new Error("WebGL framebuffer is required"); this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, this.#drawFbo.framebuffer); this.#gl.clearBufferuiv(this.#gl.COLOR, 0, [0, 0, 0, 0]); this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, null); for (let i = 0; i < this.#uboView.byteLength; i++) this.#uboView.setUint8(i, 0); for (let i = 0; i < 64; i += 8) { const uint32 = hash.slice(i, i + 8); this.#uboView.setUint32(i * 2, parseInt(uint32, 16)); } this.#uboView.setBigUint64(128, difficulty, true); this.#uboView.setUint32(136, 0, true); if (this.#debug) console.log("UBO", this.#uboView.buffer.slice(0)); this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#uboBuffer); this.#gl.bufferSubData(this.#gl.UNIFORM_BUFFER, 0, this.#uboView); this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null); let times = []; let start = performance.now(); let result = 0n; let nonce = 0n; let found = false; if (this.#debug) console.groupCollapsed("Seeds (click to view)"); while (!found) { start = performance.now(); const random0 = Math.floor(Math.random() * 4294967295); const random1 = Math.floor(Math.random() * 4294967295); this.#seed[0] = BigInt(random0) << 32n | BigInt(random1); if (this.#debug) console.log("Seed", this.#seed); this.#draw(this.#seed); found = await this.#checkQueryResult(); times.push(performance.now() - start); if (found) { ({ result, nonce } = this.#readResult()); if (this.#debug) console.groupEnd(); } } this.#busy = false; if (this.#debug) this.#logAverages(times); return { hash, work: nonce.toString(16).padStart(16, "0"), difficulty: result.toString(16).padStart(16, "0") }; } /** * 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 work_validate(work, hash, options) { 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) => { setTimeout(async () => { const result2 = this.work_validate(work, hash, options); resolve(result2); }, 500); }); } if (this.#isInitialized === false) this.init(); options ??= {}; options.debug ??= false; options.difficulty ??= 0xfffffff800000000n; options.effort ??= 4; if (typeof options?.difficulty !== "string" && typeof options?.difficulty !== "bigint") { throw new TypeError(`Invalid difficulty ${options?.difficulty}`); } 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)); if (_NanoPowGl.#gl == null) throw new Error("WebGL 2 is required"); if (this.#gl == null) throw new Error("WebGL 2 is required"); if (this.#drawFbo == null) throw new Error("WebGL framebuffer is required"); this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, this.#drawFbo.framebuffer); this.#gl.clearBufferuiv(this.#gl.COLOR, 0, [0, 0, 0, 0]); this.#gl.bindFramebuffer(this.#gl.FRAMEBUFFER, null); for (let i = 0; i < this.#uboView.byteLength; i++) this.#uboView.setUint8(i, 0); for (let i = 0; i < 64; i += 8) { const uint32 = hash.slice(i, i + 8); this.#uboView.setUint32(i * 2, parseInt(uint32, 16)); } this.#uboView.setBigUint64(128, difficulty, true); this.#uboView.setUint32(136, 1, true); if (this.#debug) console.log("UBO", this.#uboView.buffer.slice(0)); this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#uboBuffer); this.#gl.bufferSubData(this.#gl.UNIFORM_BUFFER, 0, this.#uboView); this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null); let result = 0n; let nonce = 0n; this.#seed[0] = BigInt(`0x${work}`); if (this.#debug) console.log("Work", this.#seed); this.#draw(this.#seed); let found = await this.#checkQueryResult(); if (found) { try { ({ result, nonce } = this.#readResult(work)); } catch (err) { throw new Error("Error reading results", { cause: err }); } } else { throw new Error("Failed to find nonce on canvas"); } this.#busy = false; if (this.#debug) console.log("result", result, result.toString(16).padStart(16, "0")); if (this.#debug) console.log("nonce", nonce, nonce.toString(16).padStart(16, "0")); const response = { 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; } }; // src/lib/gpu/compute.wgsl var compute_default = `//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later struct UBO{blockhash:array<vec4<u32>,2>,difficulty:vec2<u32>,validate:u32,seed:vec2<u32>};@group(0)@binding(0)var<uniform> ubo:UBO;struct WORK{found:atomic<u32>,nonce:vec2<u32>,result:vec2<u32>};@group(0)@binding(1)var<storage,read_write>work:WORK;const BLAKE2B_IV=array<vec2<u32>,8>(vec2<u32>(0xF3BCC908u,0x6A09E667u),vec2<u32>(0x84CAA73Bu,0xBB67AE85u),vec2<u32>(0xFE94F82Bu,0x3C6EF372u),vec2<u32>(0x5F1D36F1u,0xA54FF53Au),vec2<u32>(0xADE682D1u,0x510E527Fu),vec2<u32>(0x2B3E6C1Fu,0x9B05688Cu),vec2<u32>(0xFB41BD6Bu,0x1F83D9ABu),vec2<u32>(0x137E2179u,0x5BE0CD19u));const BLAKE2B_PARAM=vec2<u32>(0x01010008u,0u);const BLAKE2B_INLEN=vec2<u32>(0x00000028u,0u);const BLAKE2B_FINAL=vec2<u32>(0xFFFFFFFFu,0xFFFFFFFFu);const BLAKE2B_INIT=array<vec2<u32>,16>(BLAKE2B_IV[0u]^ BLAKE2B_PARAM,BLAKE2B_IV[1u],BLAKE2B_IV[2u],BLAKE2B_IV[3u],BLAKE2B_IV[4u],BLAKE2B_IV[5u],BLAKE2B_IV[6u],BLAKE2B_IV[7u],BLAKE2B_IV[0u],BLAKE2B_IV[1u],BLAKE2B_IV[2u],BLAKE2B_IV[3u],BLAKE2B_IV[4u]^ BLAKE2B_INLEN,BLAKE2B_IV[5u],BLAKE2B_IV[6u]^ BLAKE2B_FINAL,BLAKE2B_IV[7u]);fn G(a:ptr<function,vec2<u32>>,b:ptr<function,vec2<u32>>,c:ptr<function,vec2<u32>>,d:ptr<function,vec2<u32>>,m0:vec2<u32>,m1:vec2<u32>){*a+=*b;(*a).y+=u32((*a).x<(*b).x);*a+=m0;(*a).y+=u32((*a).x<m0.x);*d=(*d ^*a).yx;*c+=*d;(*c).y+=u32((*c).x<(*d).x);*b ^=*c;*b=(*b>>vec2(24u))|(*b<<vec2(8u)).yx;*a+=*b;(*a).y+=u32((*a).x<(*b).x);*a+=m1;(*a).y+=u32((*a).x<m1.x);*d ^=*a;*d=(*d>>vec2(16u))|(*d<<vec2(16u)).yx;*c+=*d;(*c).y+=u32((*c).x<(*d).x);*b ^=*c;*b=(*b>>vec2(31u)).yx|(*b<<vec2(1u));}const Z=vec2(0u);var<workgroup> validate:bool;var<workgroup> found:bool;var<workgroup> seed:vec2<u32>;var<workgroup> m1:vec2<u32>;var<workgroup> m2:vec2<u32>;var<workgroup> m3:vec2<u32>;var<workgroup> m4:vec2<u32>;@compute @workgroup_size(64)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;m2=ubo.blockhash[0u].zw;m3=ubo.blockhash[1u].xy;m4=ubo.blockhash[1u].zw;}workgroupBarrier();if(found){return;}let m0:vec2<u32>=seed ^ global_id.xy;var v0:vec2<u32>=vec2<u32>(BLAKE2B_INIT[0u]);var v1:vec2<u32>=vec2<u32>(BLAKE2B_INIT[1u]);var v2:vec2<u32>=vec2<u32>(BLAKE2B_INIT[2u]);var v3:vec2<u32>=vec2<u32>(BLAKE2B_INIT[3u]);var v4:vec2<u32>=vec2<u32>(BLAKE2B_INIT[4u]);var v5:vec2<u32>=vec2<u32>(BLAKE2B_INIT[5u]);var v6:vec2<u32>=vec2<u32>(BLAKE2B_INIT[6u]);var v7:vec2<u32>=vec2<u32>(BLAKE2B_INIT[7u]);var v8:vec2<u32>=vec2<u32>(BLAKE2B_INIT[8u]);var v9:vec2<u32>=vec2<u32>(BLAKE2B_INIT[9u]);var vA:vec2<u32>=vec2<u32>(BLAKE2B_INIT[10u]);var vB:vec2<u32>=vec2<u32>(BLAKE2B_INIT[11u]);var vC:vec2<u32>=vec2<u32>(BLAKE2B_INIT[12u]);var vD:vec2<u32>=vec2<u32>(BLAKE2B_INIT[13u]);var vE:vec2<u32>=vec2<u32>(BLAKE2B_INIT[14u]);var vF:vec2<u32>=vec2<u32>(BLAKE2B_INIT[15u]);G(&v0,&v4,&v8,&vC,m0,m1);G(&v1,&v5,&v9,&vD,m2,m3);G(&v2,&v6,&vA,&vE,m4,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,Z,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,m4,Z);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,m1,Z);G(&v1,&v6,&vB,&vC,m0,m2);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,Z,m3);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,Z,m0);G(&v2,&v6,&vA,&vE,Z,m2);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,Z,Z);G(&v1,&v6,&vB,&vC,m3,Z);G(&v2,&v7,&v8,&vD,Z,m1);G(&v3,&v4,&v9,&vE,Z,m4);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,m3,m1);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,m2,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,m4,m0);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,m0);G(&v1,&v5,&v9,&vD,Z,Z);G(&v2,&v6,&vA,&vE,m2,m4);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,Z,m1);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,m3,Z);G(&v0,&v4,&v8,&vC,m2,Z);G(&v1,&v5,&v9,&vD,Z,Z);G(&v2,&v6,&vA,&vE,m0,Z);G(&v3,&v7,&vB,&vF,Z,m3);G(&v0,&v5,&vA,&vF,m4,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,m1,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,m1,Z);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,m4,Z);G(&v0,&v5,&vA,&vF,m0,Z);G(&v1,&v6,&vB,&vC,Z,m3);G(&v2,&v7,&v8,&vD,Z,m2);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,Z,Z);G(&v2,&v6,&vA,&vE,Z,m1);G(&v3,&v7,&vB,&vF,m3,Z);G(&v0,&v5,&vA,&vF,Z,m0);G(&v1,&v6,&vB,&vC,Z,m4);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,m2,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,Z,Z);G(&v2,&v6,&vA,&vE,Z,m3);G(&v3,&v7,&vB,&vF,m0,Z);G(&v0,&v5,&vA,&vF,Z,m2);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,m1,m4);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,m2);G(&v1,&v5,&v9,&vD,Z,m4);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,m1,Z);G(&v0,&v5,&vA,&vF,Z,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,m3,Z);G(&v3,&v4,&v9,&vE,Z,m0);G(&v0,&v4,&v8,&vC,m0,m1);G(&v1,&v5,&v9,&vD,m2,m3);G(&v2,&v6,&vA,&vE,m4,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,Z,Z);G(&v1,&v6,&vB,&vC,Z,Z);G(&v2,&v7,&v8,&vD,Z,Z);G(&v3,&v4,&v9,&vE,Z,Z);G(&v0,&v4,&v8,&vC,Z,Z);G(&v1,&v5,&v9,&vD,m4,Z);G(&v2,&v6,&vA,&vE,Z,Z);G(&v3,&v7,&vB,&vF,Z,Z);G(&v0,&v5,&vA,&vF,m1,Z);G(&v2,&v7,&v8,&vD,Z,Z);var result:vec2<u32>=BLAKE2B_INIT[0u]^ v0 ^ v8;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;}`; // src/lib/gpu/index.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later var NanoPowGpu = class _NanoPowGpu { static #SEND = 0xfffffff800000000n; static #RECEIVE = 0xfffffe0000000000n; // Initialize WebGPU static #isInitialized = false; static #debug = false; static #action = "work_generate"; static #difficulty = this.#SEND; static #effort = 4; static #queue = []; static #device = null; static #bufferReset = new BigUint64Array(4); static #uboArray = new BigUint64Array(8); static #uboView; static #uboBuffer; static #gpuBuffer; static #cpuBuffer; static #bindGroupLayout; static #bindGroup; static #pipeline; static #resultView; static #result; // Initialize WebGPU static async init() { console.log("Initializing NanoPowGpu."); try { if (navigator.gpu == null) throw new Error("WebGPU is not supported in this browser."); const adapter = await navigator.gpu.requestAdapter(); 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."); device.lost?.then(this.reset); this.#device = device; 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 }); this.#bindGroupLayout = this.#device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } } ] }); this.#pipeline = this.#device.createComputePipeline({ layout: this.#device.createPipelineLayout({ bindGroupLayouts: [this.#bindGroupLayout] }), compute: { entryPoint: "main", module: this.#device.createShaderModule({ code: compute_default }) } }); this.#bindGroup = this.#device.createBindGroup({ layout: this.#bindGroupLayout, entries: [ { binding: 0, resource: { buffer: this.#uboBuffer } }, { binding: 1, resource: { buffer: this.#gpuBuffer } } ] }); 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 }); } } static reset() { console.warn(`GPU device lost. Reinitializing...`); _NanoPowGpu.#cpuBuffer?.destroy(); _NanoPowGpu.#gpuBuffer?.destroy(); _NanoPowGpu.#uboBuffer?.destroy(); _NanoPowGpu.#bindGroupLayout = null; _NanoPowGpu.#bindGroup = null; _NanoPowGpu.#pipeline = null; _NanoPowGpu.#isInitialized = false; queueMicrotask(() => _NanoPowGpu.init().catch(console.log)); } /** * Validate work, if present, and blockhash. * Validate options and normalize its properties. */ static async #work_init(work, hash, options) { 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 ??= 4; if (typeof options.effort !== "number" || options.effort < 1 || options.effort > 32) { throw new TypeError(`Invalid effort ${options.effort}`); } this.#effort = this.#action === "work_generate" ? options.effort * 256 : 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}`); } 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)); } 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.`); } this.#device.queue.writeBuffer(this.#gpuBuffer, 0, this.#bufferReset); this.#device.queue.writeBuffer(this.#cpuBuffer, 0, this.#bufferReset); 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, hash) { 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")); this.#uboView.setBigUint64(48, seed, true); if (this.#debug) console.log("UBO", this.#uboView); this.#device.queue.writeBuffer(this.#uboBuffer, 0, this.#uboView); const commandEncoder = this.#device.createCommandEncoder(); const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline(this.#pipeline); passEncoder.setBindGroup(0, this.#bindGroup); passEncoder.dispatchWorkgroups(this.#effort, this.#effort); passEncoder.end(); commandEncoder.copyBufferToBuffer(this.#gpuBuffer, 0, this.#cpuBuffer, 0, 32); this.#device.queue.submit([commandEncoder.finish()]); try { await this.#cpuBuffer.mapAsync(GPUMapMode.READ); await this.#device.queue.onSubmittedWorkDone(); this.#resultView = new DataView(this.#cpuBuffer.getMappedRange().slice(0)); this.#cpuBuffer.unmap(); } catch (err) { console.warn(`Error getting data from GPU. ${err}`); this.#cpuBuffer.unmap(); this.reset(); } 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() { const { task, resolve, reject } = this.#queue.shift() ?? {}; task?.().then(resolve).catch(reject).finally(() => { this.#work_process(); }); } static async #work_queue(task) { return new Promise((resolve, reject) => { this.#queue.push({ task, resolve, reject }); this.#work_process(); }); } /** * 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 work_generate(hash, options) { return this.#work_queue(async () => { this.#action = "work_generate"; await this.#work_init(null, hash, options); let random = BigInt(Math.floor(Math.random() * 4294967295)); let seed = random; do { random = BigInt(Math.floor(Math.random() * 4294967295)); seed = (seed & 0xffffffffn) << 32n | random; await this.#work_dispatch(seed, hash); } while (!this.#result.found); return { hash, work: this.#result.work.toString(16).padStart(16, "0"), difficulty: this.#result.difficulty.toString(16).padStart(16, "0") }; }); } /** * 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 work_validate(work, hash, options) { return this.#work_queue(async () => { this.#action = "work_validate"; await this.#work_init(work, hash, options); const seed = BigInt(`0x${work}`); await this.#work_dispatch(seed, hash); if (seed !== this.#result.work) throw new Error("Result does not match work"); const response = { 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; }); } }; // src/lib/index.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later var isGlSupported; var isGpuSupported = false; try { const adapter = await navigator?.gpu?.requestAdapter?.(); isGpuSupported = adapter instanceof GPUAdapter; } catch (err) { console.warn("WebGPU is not supported in this environment.\n", err); isGpuSupported = false; } try { const gl = new OffscreenCanvas(0, 0)?.getContext?.("webgl2"); isGlSupported = gl instanceof WebGL2RenderingContext; } catch (err) { console.warn("WebGL is not supported in this environment.\n", err); isGlSupported = false; } var NanoPow = isGpuSupported ? NanoPowGpu : isGlSupported ? NanoPowGl : null; // src/main.ts //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev> //! SPDX-License-Identifier: GPL-3.0-or-later export { NanoPow, NanoPowGl, NanoPowGpu, NanoPow as default };