nano-pow
Version:
Proof-of-work generation and validation with WebGPU/WebGL for Nano cryptocurrency.
817 lines (808 loc) • 47.6 kB
JavaScript
// 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>};var<uniform> ubo:UBO;struct WORK{found:atomic<u32>,nonce:vec2<u32>,result:vec2<u32>};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>; fn main( global_id:vec3<u32>, 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
};