nano-pow 1.2.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/classes/gl.d.ts +20 -0
- package/dist/classes/gl.js +232 -0
- package/dist/classes/gpu.d.ts +24 -0
- package/dist/classes/gpu.js +236 -0
- package/dist/classes/index.d.ts +4 -0
- package/dist/classes/index.js +23 -0
- package/dist/global.d.ts +1 -0
- package/dist/global.js +6 -0
- package/dist/global.min.js +228 -101
- package/dist/main.d.ts +3 -0
- package/dist/main.js +5 -0
- package/dist/main.min.js +228 -101
- package/dist/shaders/gl-fragment.d.ts +1 -0
- package/dist/shaders/gl-fragment.js +206 -0
- package/dist/shaders/gl-vertex.d.ts +1 -0
- package/dist/shaders/gl-vertex.js +16 -0
- package/dist/shaders/index.d.ts +4 -0
- package/dist/shaders/index.js +6 -0
- package/dist/types.d.ts +30 -10
- package/package.json +1 -2
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare class NanoPowGl {
|
|
2
|
+
#private;
|
|
3
|
+
/** Compile */
|
|
4
|
+
static init(): Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* Finds a nonce that satisfies the Nano proof-of-work requirements.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
|
|
9
|
+
* @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation
|
|
10
|
+
*/
|
|
11
|
+
static search(hash: string, threshold?: number): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Validates that a nonce satisfies Nano proof-of-work requirements.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} work - Hexadecimal proof-of-work value to validate
|
|
16
|
+
* @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
|
|
17
|
+
* @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation
|
|
18
|
+
*/
|
|
19
|
+
static validate(work: string, hash: string, threshold?: number): Promise<boolean>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
|
|
2
|
+
// SPDX-FileContributor: Ben Green <ben@latenightsketches.com>
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later AND MIT
|
|
4
|
+
import { NanoPowGlFragmentShader, NanoPowGlVertexShader } from '#shaders';
|
|
5
|
+
export class NanoPowGl {
|
|
6
|
+
/** Used to set canvas size. Must be a multiple of 256. */
|
|
7
|
+
static #WORKLOAD = 256 * Math.max(1, Math.floor(navigator.hardwareConcurrency));
|
|
8
|
+
static #hexify(arr) {
|
|
9
|
+
let out = '';
|
|
10
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
11
|
+
out += arr[i].toString(16).padStart(2, '0');
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
static #gl;
|
|
16
|
+
static #program;
|
|
17
|
+
static #vertexShader;
|
|
18
|
+
static #fragmentShader;
|
|
19
|
+
static #positionBuffer;
|
|
20
|
+
static #uvBuffer;
|
|
21
|
+
static #uboBuffer;
|
|
22
|
+
static #workBuffer;
|
|
23
|
+
static #query;
|
|
24
|
+
static #pixels;
|
|
25
|
+
/**Vertex Positions, 2 triangles */
|
|
26
|
+
static #positions = new Float32Array([
|
|
27
|
+
-1, -1, 0, -1, 1, 0, 1, 1, 0,
|
|
28
|
+
1, -1, 0, 1, 1, 0, -1, -1, 0
|
|
29
|
+
]);
|
|
30
|
+
/** Texture Positions */
|
|
31
|
+
static #uvPosArray = new Float32Array([
|
|
32
|
+
1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1
|
|
33
|
+
]);
|
|
34
|
+
/** Compile */
|
|
35
|
+
static async init() {
|
|
36
|
+
this.#gl = new OffscreenCanvas(this.#WORKLOAD, this.#WORKLOAD).getContext('webgl2');
|
|
37
|
+
if (this.#gl == null)
|
|
38
|
+
throw new Error('WebGL 2 is required');
|
|
39
|
+
this.#gl.clearColor(0, 0, 0, 1);
|
|
40
|
+
this.#program = this.#gl.createProgram();
|
|
41
|
+
if (this.#program == null)
|
|
42
|
+
throw new Error('Failed to create shader program');
|
|
43
|
+
this.#vertexShader = this.#gl.createShader(this.#gl.VERTEX_SHADER);
|
|
44
|
+
if (this.#vertexShader == null)
|
|
45
|
+
throw new Error('Failed to create vertex shader');
|
|
46
|
+
this.#gl.shaderSource(this.#vertexShader, NanoPowGlVertexShader);
|
|
47
|
+
this.#gl.compileShader(this.#vertexShader);
|
|
48
|
+
if (!this.#gl.getShaderParameter(this.#vertexShader, this.#gl.COMPILE_STATUS))
|
|
49
|
+
throw new Error(this.#gl.getShaderInfoLog(this.#vertexShader) ?? `Failed to compile vertex shader`);
|
|
50
|
+
this.#fragmentShader = this.#gl.createShader(this.#gl.FRAGMENT_SHADER);
|
|
51
|
+
if (this.#fragmentShader == null)
|
|
52
|
+
throw new Error('Failed to create fragment shader');
|
|
53
|
+
this.#gl.shaderSource(this.#fragmentShader, NanoPowGlFragmentShader);
|
|
54
|
+
this.#gl.compileShader(this.#fragmentShader);
|
|
55
|
+
if (!this.#gl.getShaderParameter(this.#fragmentShader, this.#gl.COMPILE_STATUS))
|
|
56
|
+
throw new Error(this.#gl.getShaderInfoLog(this.#fragmentShader) ?? `Failed to compile fragment shader`);
|
|
57
|
+
this.#gl.attachShader(this.#program, this.#vertexShader);
|
|
58
|
+
this.#gl.attachShader(this.#program, this.#fragmentShader);
|
|
59
|
+
this.#gl.linkProgram(this.#program);
|
|
60
|
+
if (!this.#gl.getProgramParameter(this.#program, this.#gl.LINK_STATUS))
|
|
61
|
+
throw new Error(this.#gl.getProgramInfoLog(this.#program) ?? `Failed to link program`);
|
|
62
|
+
/** Construct simple 2D geometry */
|
|
63
|
+
this.#gl.useProgram(this.#program);
|
|
64
|
+
const triangleArray = this.#gl.createVertexArray();
|
|
65
|
+
this.#gl.bindVertexArray(triangleArray);
|
|
66
|
+
this.#positionBuffer = this.#gl.createBuffer();
|
|
67
|
+
this.#gl.bindBuffer(this.#gl.ARRAY_BUFFER, this.#positionBuffer);
|
|
68
|
+
this.#gl.bufferData(this.#gl.ARRAY_BUFFER, this.#positions, this.#gl.STATIC_DRAW);
|
|
69
|
+
this.#gl.vertexAttribPointer(0, 3, this.#gl.FLOAT, false, 0, 0);
|
|
70
|
+
this.#gl.enableVertexAttribArray(0);
|
|
71
|
+
this.#uvBuffer = this.#gl.createBuffer();
|
|
72
|
+
this.#gl.bindBuffer(this.#gl.ARRAY_BUFFER, this.#uvBuffer);
|
|
73
|
+
this.#gl.bufferData(this.#gl.ARRAY_BUFFER, this.#uvPosArray, this.#gl.STATIC_DRAW);
|
|
74
|
+
this.#gl.vertexAttribPointer(1, 2, this.#gl.FLOAT, false, 0, 0);
|
|
75
|
+
this.#gl.enableVertexAttribArray(1);
|
|
76
|
+
this.#uboBuffer = this.#gl.createBuffer();
|
|
77
|
+
this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#uboBuffer);
|
|
78
|
+
this.#gl.bufferData(this.#gl.UNIFORM_BUFFER, 144, this.#gl.DYNAMIC_DRAW);
|
|
79
|
+
this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null);
|
|
80
|
+
this.#gl.bindBufferBase(this.#gl.UNIFORM_BUFFER, 0, this.#uboBuffer);
|
|
81
|
+
this.#gl.uniformBlockBinding(this.#program, this.#gl.getUniformBlockIndex(this.#program, 'UBO'), 0);
|
|
82
|
+
this.#workBuffer = this.#gl.createBuffer();
|
|
83
|
+
this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#workBuffer);
|
|
84
|
+
this.#gl.bufferData(this.#gl.UNIFORM_BUFFER, 32, this.#gl.STREAM_DRAW);
|
|
85
|
+
this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null);
|
|
86
|
+
this.#gl.bindBufferBase(this.#gl.UNIFORM_BUFFER, 1, this.#workBuffer);
|
|
87
|
+
this.#gl.uniformBlockBinding(this.#program, this.#gl.getUniformBlockIndex(this.#program, 'WORK'), 1);
|
|
88
|
+
this.#pixels = new Uint8Array(this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight * 4);
|
|
89
|
+
this.#query = this.#gl.createQuery();
|
|
90
|
+
}
|
|
91
|
+
static #draw(work) {
|
|
92
|
+
if (this.#gl == null || this.#query == null)
|
|
93
|
+
throw new Error('WebGL 2 is required to draw and query pixels');
|
|
94
|
+
if (this.#workBuffer == null)
|
|
95
|
+
throw new Error('Work buffer is required to draw');
|
|
96
|
+
this.#gl.clear(this.#gl.COLOR_BUFFER_BIT);
|
|
97
|
+
/** Upload work buffer */
|
|
98
|
+
this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#workBuffer);
|
|
99
|
+
this.#gl.bufferSubData(this.#gl.UNIFORM_BUFFER, 0, Uint32Array.from(work));
|
|
100
|
+
this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null);
|
|
101
|
+
this.#gl.beginQuery(this.#gl.ANY_SAMPLES_PASSED_CONSERVATIVE, this.#query);
|
|
102
|
+
this.#gl.drawArrays(this.#gl.TRIANGLES, 0, 6);
|
|
103
|
+
this.#gl.endQuery(this.#gl.ANY_SAMPLES_PASSED_CONSERVATIVE);
|
|
104
|
+
}
|
|
105
|
+
static async #checkQueryResult() {
|
|
106
|
+
if (this.#gl == null || this.#query == null)
|
|
107
|
+
throw new Error('WebGL 2 is required to check query results');
|
|
108
|
+
if (this.#gl.getQueryParameter(this.#query, this.#gl.QUERY_RESULT_AVAILABLE)) {
|
|
109
|
+
return !!(this.#gl.getQueryParameter(this.#query, this.#gl.QUERY_RESULT));
|
|
110
|
+
}
|
|
111
|
+
/** Query result not yet available, check again in the next frame */
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
try {
|
|
114
|
+
requestAnimationFrame(async () => {
|
|
115
|
+
const result = await NanoPowGl.#checkQueryResult();
|
|
116
|
+
resolve(result);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
reject(err);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Reads pixels into the work buffer, checks every 4th pixel for the 'found'
|
|
126
|
+
* byte, converts the subsequent 3 pixels with the nonce byte values to a hex
|
|
127
|
+
* string, and returns the result.
|
|
128
|
+
*
|
|
129
|
+
* @param work - Buffer with the original random nonce value
|
|
130
|
+
* @returns Nonce as an 8-byte (16-char) hexadecimal string
|
|
131
|
+
*/
|
|
132
|
+
static #readResult(work) {
|
|
133
|
+
if (this.#gl == null)
|
|
134
|
+
throw new Error('WebGL 2 is required to read pixels');
|
|
135
|
+
this.#gl.readPixels(0, 0, this.#gl.drawingBufferWidth, this.#gl.drawingBufferHeight, this.#gl.RGBA, this.#gl.UNSIGNED_BYTE, this.#pixels);
|
|
136
|
+
for (let i = 0; i < this.#pixels.length; i += 4) {
|
|
137
|
+
if (this.#pixels[i] !== 0) {
|
|
138
|
+
/** Return the work value with the custom bits */
|
|
139
|
+
const hex = this.#hexify(work.subarray(4, 8)) + this.#hexify([
|
|
140
|
+
this.#pixels[i + 2],
|
|
141
|
+
this.#pixels[i + 3],
|
|
142
|
+
work[2] ^ (this.#pixels[i] - 1),
|
|
143
|
+
work[3] ^ (this.#pixels[i + 1] - 1)
|
|
144
|
+
]);
|
|
145
|
+
return hex;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
throw new Error('Query reported result but nonce value not found');
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Finds a nonce that satisfies the Nano proof-of-work requirements.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
|
|
154
|
+
* @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation
|
|
155
|
+
*/
|
|
156
|
+
static async search(hash, threshold = 0xfffffff8) {
|
|
157
|
+
if (NanoPowGl.#gl == null)
|
|
158
|
+
throw new Error('WebGL 2 is required');
|
|
159
|
+
if (!/^[A-Fa-f0-9]{64}$/.test(hash))
|
|
160
|
+
throw new Error(`Invalid hash ${hash}`);
|
|
161
|
+
if (typeof threshold !== 'number')
|
|
162
|
+
throw new TypeError(`Invalid threshold ${threshold}`);
|
|
163
|
+
if (this.#gl == null)
|
|
164
|
+
throw new Error('WebGL 2 is required');
|
|
165
|
+
/** Set up uniform buffer object */
|
|
166
|
+
const uboView = new DataView(new ArrayBuffer(144));
|
|
167
|
+
for (let i = 0; i < 64; i += 8) {
|
|
168
|
+
const uint32 = hash.slice(i, i + 8);
|
|
169
|
+
uboView.setUint32(i * 2, parseInt(uint32, 16));
|
|
170
|
+
}
|
|
171
|
+
uboView.setUint32(128, threshold, true);
|
|
172
|
+
uboView.setFloat32(132, NanoPowGl.#WORKLOAD - 1, true);
|
|
173
|
+
NanoPowGl.#gl.bindBuffer(NanoPowGl.#gl.UNIFORM_BUFFER, NanoPowGl.#uboBuffer);
|
|
174
|
+
NanoPowGl.#gl.bufferSubData(NanoPowGl.#gl.UNIFORM_BUFFER, 0, uboView);
|
|
175
|
+
NanoPowGl.#gl.bindBuffer(NanoPowGl.#gl.UNIFORM_BUFFER, null);
|
|
176
|
+
/** Start drawing to calculate one nonce per pixel */
|
|
177
|
+
let nonce = null;
|
|
178
|
+
const seed = new Uint8Array(8);
|
|
179
|
+
while (nonce == null) {
|
|
180
|
+
crypto.getRandomValues(seed);
|
|
181
|
+
this.#draw(seed);
|
|
182
|
+
const found = await this.#checkQueryResult();
|
|
183
|
+
if (found) {
|
|
184
|
+
nonce = this.#readResult(seed);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return nonce;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Validates that a nonce satisfies Nano proof-of-work requirements.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} work - Hexadecimal proof-of-work value to validate
|
|
193
|
+
* @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
|
|
194
|
+
* @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation
|
|
195
|
+
*/
|
|
196
|
+
static async validate(work, hash, threshold = 0xfffffff8) {
|
|
197
|
+
if (NanoPowGl.#gl == null)
|
|
198
|
+
throw new Error('WebGL 2 is required');
|
|
199
|
+
if (!/^[A-Fa-f0-9]{16}$/.test(work))
|
|
200
|
+
throw new Error(`Invalid work ${work}`);
|
|
201
|
+
if (!/^[A-Fa-f0-9]{64}$/.test(hash))
|
|
202
|
+
throw new Error(`Invalid hash ${hash}`);
|
|
203
|
+
if (typeof threshold !== 'number')
|
|
204
|
+
throw new TypeError(`Invalid threshold ${threshold}`);
|
|
205
|
+
if (this.#gl == null)
|
|
206
|
+
throw new Error('WebGL 2 is required');
|
|
207
|
+
/** Set up uniform buffer object */
|
|
208
|
+
const uboView = new DataView(new ArrayBuffer(144));
|
|
209
|
+
for (let i = 0; i < 64; i += 8) {
|
|
210
|
+
const uint32 = hash.slice(i, i + 8);
|
|
211
|
+
uboView.setUint32(i * 2, parseInt(uint32, 16));
|
|
212
|
+
}
|
|
213
|
+
uboView.setUint32(128, threshold, true);
|
|
214
|
+
uboView.setFloat32(132, NanoPowGl.#WORKLOAD - 1, true);
|
|
215
|
+
NanoPowGl.#gl.bindBuffer(NanoPowGl.#gl.UNIFORM_BUFFER, NanoPowGl.#uboBuffer);
|
|
216
|
+
NanoPowGl.#gl.bufferSubData(NanoPowGl.#gl.UNIFORM_BUFFER, 0, uboView);
|
|
217
|
+
NanoPowGl.#gl.bindBuffer(NanoPowGl.#gl.UNIFORM_BUFFER, null);
|
|
218
|
+
/** Start drawing to calculate one nonce per pixel */
|
|
219
|
+
let nonce = null;
|
|
220
|
+
const data = new DataView(new ArrayBuffer(8));
|
|
221
|
+
data.setBigUint64(0, BigInt(`0x${work}`), true);
|
|
222
|
+
const seed = new Uint8Array(data.buffer);
|
|
223
|
+
this.#draw(seed);
|
|
224
|
+
const found = await this.#checkQueryResult();
|
|
225
|
+
if (found) {
|
|
226
|
+
nonce = this.#readResult(seed);
|
|
227
|
+
}
|
|
228
|
+
if (found && nonce !== work)
|
|
229
|
+
throw new Error(`Nonce found but does not match work`);
|
|
230
|
+
return found;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nano proof-of-work using WebGPU.
|
|
3
|
+
*/
|
|
4
|
+
export declare class NanoPowGpu {
|
|
5
|
+
#private;
|
|
6
|
+
static init(): Promise<void>;
|
|
7
|
+
static setup(): void;
|
|
8
|
+
static reset(): void;
|
|
9
|
+
/**
|
|
10
|
+
* Finds a nonce that satisfies the Nano proof-of-work requirements.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
|
|
13
|
+
* @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation
|
|
14
|
+
*/
|
|
15
|
+
static search(hash: string, threshold?: number): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Validates that a nonce satisfies Nano proof-of-work requirements.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} work - Hexadecimal proof-of-work value to validate
|
|
20
|
+
* @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
|
|
21
|
+
* @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation
|
|
22
|
+
*/
|
|
23
|
+
static validate(work: string, hash: string, threshold?: number): Promise<boolean>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
/// <reference types="@webgpu/types" />
|
|
4
|
+
import { NanoPowGpuComputeShader } from '#shaders';
|
|
5
|
+
/**
|
|
6
|
+
* Nano proof-of-work using WebGPU.
|
|
7
|
+
*/
|
|
8
|
+
export class NanoPowGpu {
|
|
9
|
+
// Initialize WebGPU
|
|
10
|
+
static #busy = false;
|
|
11
|
+
static #device = null;
|
|
12
|
+
static #uboBuffer;
|
|
13
|
+
static #gpuBuffer;
|
|
14
|
+
static #cpuBuffer;
|
|
15
|
+
static #bindGroupLayout;
|
|
16
|
+
static #pipeline;
|
|
17
|
+
// Initialize WebGPU
|
|
18
|
+
static async init() {
|
|
19
|
+
if (this.#busy)
|
|
20
|
+
return;
|
|
21
|
+
this.#busy = true;
|
|
22
|
+
// Request device and adapter
|
|
23
|
+
if (navigator.gpu == null)
|
|
24
|
+
throw new Error('WebGPU is not supported in this browser.');
|
|
25
|
+
try {
|
|
26
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
27
|
+
if (adapter == null)
|
|
28
|
+
throw new Error('WebGPU adapter refused by browser.');
|
|
29
|
+
const device = await adapter.requestDevice();
|
|
30
|
+
if (!(device instanceof GPUDevice))
|
|
31
|
+
throw new Error('WebGPU device failed to load.');
|
|
32
|
+
device.lost.then(this.reset);
|
|
33
|
+
this.#device = device;
|
|
34
|
+
this.setup();
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
throw new Error(`WebGPU initialization failed. ${err}`);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
this.#busy = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
static setup() {
|
|
44
|
+
if (this.#device == null)
|
|
45
|
+
throw new Error(`WebGPU device failed to load.`);
|
|
46
|
+
// Create buffers for writing GPU calculations and reading from Javascript
|
|
47
|
+
this.#uboBuffer = this.#device.createBuffer({
|
|
48
|
+
size: 48,
|
|
49
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
50
|
+
});
|
|
51
|
+
this.#gpuBuffer = this.#device.createBuffer({
|
|
52
|
+
size: 16,
|
|
53
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
|
|
54
|
+
});
|
|
55
|
+
this.#cpuBuffer = this.#device.createBuffer({
|
|
56
|
+
size: 16,
|
|
57
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
58
|
+
});
|
|
59
|
+
// Create binding group data structure and use it later once UBO is known
|
|
60
|
+
this.#bindGroupLayout = this.#device.createBindGroupLayout({
|
|
61
|
+
entries: [
|
|
62
|
+
{
|
|
63
|
+
binding: 0,
|
|
64
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
65
|
+
buffer: { type: 'uniform' },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
binding: 1,
|
|
69
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
70
|
+
buffer: { type: 'storage' },
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
});
|
|
74
|
+
// Create pipeline to connect compute shader to binding layout
|
|
75
|
+
this.#pipeline = this.#device.createComputePipeline({
|
|
76
|
+
layout: this.#device.createPipelineLayout({
|
|
77
|
+
bindGroupLayouts: [this.#bindGroupLayout]
|
|
78
|
+
}),
|
|
79
|
+
compute: {
|
|
80
|
+
entryPoint: 'main',
|
|
81
|
+
module: this.#device.createShaderModule({
|
|
82
|
+
code: NanoPowGpuComputeShader
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
static reset() {
|
|
88
|
+
console.warn(`GPU device lost. Reinitializing...`);
|
|
89
|
+
NanoPowGpu.#cpuBuffer?.destroy();
|
|
90
|
+
NanoPowGpu.#gpuBuffer?.destroy();
|
|
91
|
+
NanoPowGpu.#uboBuffer?.destroy();
|
|
92
|
+
NanoPowGpu.#busy = false;
|
|
93
|
+
NanoPowGpu.init();
|
|
94
|
+
}
|
|
95
|
+
static async #dispatch(seed, hash, threshold, passes) {
|
|
96
|
+
if (this.#device == null)
|
|
97
|
+
throw new Error(`WebGPU device failed to load.`);
|
|
98
|
+
// Set up uniform buffer object
|
|
99
|
+
// Note: u32 size is 4, but total alignment must be multiple of 16
|
|
100
|
+
const uboView = new DataView(new ArrayBuffer(48));
|
|
101
|
+
for (let i = 0; i < 64; i += 8) {
|
|
102
|
+
const uint32 = hash.slice(i, i + 8);
|
|
103
|
+
uboView.setUint32(i / 2, parseInt(uint32, 16));
|
|
104
|
+
}
|
|
105
|
+
uboView.setBigUint64(32, seed, true);
|
|
106
|
+
uboView.setUint32(40, threshold, true);
|
|
107
|
+
this.#device.queue.writeBuffer(this.#uboBuffer, 0, uboView);
|
|
108
|
+
// Reset `nonce` and `found` to 0u in WORK before each calculation
|
|
109
|
+
this.#device.queue.writeBuffer(this.#gpuBuffer, 0, new Uint32Array([0, 0, 0]));
|
|
110
|
+
// Bind UBO read and GPU write buffers
|
|
111
|
+
const bindGroup = this.#device.createBindGroup({
|
|
112
|
+
layout: this.#bindGroupLayout,
|
|
113
|
+
entries: [
|
|
114
|
+
{
|
|
115
|
+
binding: 0,
|
|
116
|
+
resource: {
|
|
117
|
+
buffer: this.#uboBuffer
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
binding: 1,
|
|
122
|
+
resource: {
|
|
123
|
+
buffer: this.#gpuBuffer
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
// Create command encoder to issue commands to GPU and initiate computation
|
|
129
|
+
const commandEncoder = this.#device.createCommandEncoder();
|
|
130
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
131
|
+
// Issue commands and end compute pass structure
|
|
132
|
+
passEncoder.setPipeline(this.#pipeline);
|
|
133
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
134
|
+
passEncoder.dispatchWorkgroups(passes, passes);
|
|
135
|
+
passEncoder.end();
|
|
136
|
+
// Copy 8-byte nonce and 4-byte found flag from GPU to CPU for reading
|
|
137
|
+
commandEncoder.copyBufferToBuffer(this.#gpuBuffer, 0, this.#cpuBuffer, 0, 12);
|
|
138
|
+
// End computation by passing array of command buffers to command queue for execution
|
|
139
|
+
this.#device.queue.submit([commandEncoder.finish()]);
|
|
140
|
+
// Read results back to Javascript and then unmap buffer after reading
|
|
141
|
+
let data = null;
|
|
142
|
+
try {
|
|
143
|
+
await this.#cpuBuffer.mapAsync(GPUMapMode.READ);
|
|
144
|
+
await this.#device.queue.onSubmittedWorkDone();
|
|
145
|
+
data = new DataView(this.#cpuBuffer.getMappedRange().slice(0));
|
|
146
|
+
this.#cpuBuffer.unmap();
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
console.warn(`Error getting data from GPU. ${err}`);
|
|
150
|
+
return this.#dispatch(seed, hash, threshold, passes);
|
|
151
|
+
}
|
|
152
|
+
if (data == null)
|
|
153
|
+
throw new Error(`Failed to get data from buffer.`);
|
|
154
|
+
return data;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Finds a nonce that satisfies the Nano proof-of-work requirements.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
|
|
160
|
+
* @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation
|
|
161
|
+
*/
|
|
162
|
+
static async search(hash, threshold = 0xfffffff8) {
|
|
163
|
+
if (this.#busy) {
|
|
164
|
+
return new Promise(resolve => {
|
|
165
|
+
setTimeout(async () => {
|
|
166
|
+
const result = this.search(hash, threshold);
|
|
167
|
+
resolve(result);
|
|
168
|
+
}, 100);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
this.#busy = true;
|
|
172
|
+
if (!/^[A-Fa-f0-9]{64}$/.test(hash))
|
|
173
|
+
throw new TypeError(`Invalid hash ${hash}`);
|
|
174
|
+
if (typeof threshold !== 'number')
|
|
175
|
+
throw new TypeError(`Invalid threshold ${threshold}`);
|
|
176
|
+
// Ensure WebGPU is initialized before calculating
|
|
177
|
+
let loads = 0;
|
|
178
|
+
while (this.#device == null && loads < 20) {
|
|
179
|
+
await new Promise(resolve => {
|
|
180
|
+
setTimeout(resolve, 500);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (this.#device == null)
|
|
184
|
+
throw new Error(`WebGPU device failed to load.`);
|
|
185
|
+
let nonce = 0n;
|
|
186
|
+
do {
|
|
187
|
+
const random = Math.floor(Math.random() * 0xffffffff);
|
|
188
|
+
const seed = (BigInt(random) << 32n) | BigInt(random);
|
|
189
|
+
const data = await this.#dispatch(seed, hash, threshold, 0xff);
|
|
190
|
+
nonce = data.getBigUint64(0, true);
|
|
191
|
+
this.#busy = !data.getUint32(8);
|
|
192
|
+
} while (this.#busy);
|
|
193
|
+
return nonce.toString(16).padStart(16, '0');
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Validates that a nonce satisfies Nano proof-of-work requirements.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} work - Hexadecimal proof-of-work value to validate
|
|
199
|
+
* @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
|
|
200
|
+
* @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation
|
|
201
|
+
*/
|
|
202
|
+
static async validate(work, hash, threshold = 0xfffffff8) {
|
|
203
|
+
if (this.#busy) {
|
|
204
|
+
return new Promise(resolve => {
|
|
205
|
+
setTimeout(async () => {
|
|
206
|
+
const result = this.validate(work, hash, threshold);
|
|
207
|
+
resolve(result);
|
|
208
|
+
}, 100);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
this.#busy = true;
|
|
212
|
+
if (!/^[A-Fa-f0-9]{16}$/.test(work))
|
|
213
|
+
throw new TypeError(`Invalid work ${work}`);
|
|
214
|
+
if (!/^[A-Fa-f0-9]{64}$/.test(hash))
|
|
215
|
+
throw new TypeError(`Invalid hash ${hash}`);
|
|
216
|
+
if (typeof threshold !== 'number')
|
|
217
|
+
throw new TypeError(`Invalid threshold ${threshold}`);
|
|
218
|
+
// Ensure WebGPU is initialized before calculating
|
|
219
|
+
let loads = 0;
|
|
220
|
+
while (this.#device == null && loads < 20) {
|
|
221
|
+
await new Promise(resolve => {
|
|
222
|
+
setTimeout(resolve, 500);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (this.#device == null)
|
|
226
|
+
throw new Error(`WebGPU device failed to load.`);
|
|
227
|
+
const seed = BigInt(`0x${work}`);
|
|
228
|
+
const data = await this.#dispatch(seed, hash, threshold, 1);
|
|
229
|
+
const nonce = data.getBigUint64(0, true).toString(16).padStart(16, '0');
|
|
230
|
+
const found = !!data.getUint32(8);
|
|
231
|
+
this.#busy = false;
|
|
232
|
+
if (found && work !== nonce)
|
|
233
|
+
throw new Error(`Nonce found but does not match work`);
|
|
234
|
+
return found;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
import { NanoPowGl } from "./gl.js";
|
|
4
|
+
import { NanoPowGpu } from "./gpu.js";
|
|
5
|
+
let isGlSupported, isGpuSupported = false;
|
|
6
|
+
try {
|
|
7
|
+
await NanoPowGl.init();
|
|
8
|
+
isGlSupported = true;
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
console.warn(`WebGL is not supported in this environment.`);
|
|
12
|
+
isGlSupported = false;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
await NanoPowGpu.init();
|
|
16
|
+
isGpuSupported = true;
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
console.warn(`WebGPU is not supported in this environment.`);
|
|
20
|
+
isGpuSupported = false;
|
|
21
|
+
}
|
|
22
|
+
const NanoPow = isGpuSupported ? NanoPowGpu : isGlSupported ? NanoPowGl : null;
|
|
23
|
+
export { NanoPow, NanoPowGl, NanoPowGpu };
|
package/dist/global.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/global.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
import { NanoPow as np, NanoPowGl as gl, NanoPowGpu as gpu } from './main.js';
|
|
4
|
+
globalThis.NanoPow ??= np;
|
|
5
|
+
globalThis.NanoPowGl ??= gl;
|
|
6
|
+
globalThis.NanoPowGpu ??= gpu;
|