1// pow-worker.js: the proof of work of the public issue form, in a dedicated
2// Worker (started by issue-form.js). It finds the smallest decimal counter n
3// such that SHA-256(nonce bytes || ":" || n) has at least `difficulty` leading
4// zero bits: the very check of the server (usecase/issue_submissions.go,
5// powHash). The nonce arrives as base64url without padding, like the server
6// sends it. Messages in: {nonce, difficulty}. Messages out:
7// {progress: tried} now and then, then {solution: "<decimal>"} or {error}.
8//
9// window-less on purpose: ermitePow is also loaded by Node for the tests
10// (jstest/issue-form.test.mjs).
11(() => {
12 const BATCH = 2048;
13 const enc = new TextEncoder();
14
15 // leadingZeroBits counts the zero bits at the start of the bytes.
16 function leadingZeroBits(bytes) {
17 let n = 0;
18 for (const b of bytes) {
19 if (b === 0) { n += 8; continue; }
20 return n + Math.clz32(b) - 24;
21 }
22 return n;
23 }
24
25 // b64urlDecode turns unpadded base64url into bytes.
26 function b64urlDecode(s) {
27 const bin = atob(String(s).replace(/-/g, '+').replace(/_/g, '/'));
28 const out = new Uint8Array(bin.length);
29 for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
30 return out;
31 }
32
33 // input is nonce || ":" || decimal(n), the bytes the server hashes.
34 function input(nonce, n) {
35 const tail = enc.encode(':' + n);
36 const out = new Uint8Array(nonce.length + tail.length);
37 out.set(nonce, 0);
38 out.set(tail, nonce.length);
39 return out;
40 }
41
42 // solve returns the smallest solution as a decimal string. digest is
43 // (Uint8Array) => Promise<ArrayBuffer-like>, crypto.subtle.digest by default.
44 // Hashes are asked a batch at a time, in order, so the first hit of the first
45 // batch that has one is the smallest. onProgress(tried) is called after each
46 // batch; it may return true to stop (solve then returns null).
47 async function solve(nonce, difficulty, { digest, batch = BATCH, onProgress } = {}) {
48 const dig = digest || ((d) => globalThis.crypto.subtle.digest('SHA-256', d));
49 for (let start = 0; ; start += batch) {
50 const sums = await Promise.all(Array.from({ length: batch }, async (_, i) => new Uint8Array(await dig(input(nonce, start + i)))));
51 for (let i = 0; i < batch; i++) {
52 if (leadingZeroBits(sums[i]) >= difficulty) return String(start + i);
53 }
54 if (onProgress && onProgress(start + batch)) return null;
55 }
56 }
57
58 globalThis.ermitePow = { leadingZeroBits, b64urlDecode, input, solve };
59
60 if (typeof WorkerGlobalScope !== 'undefined' && typeof globalThis.postMessage === 'function') {
61 globalThis.onmessage = async (e) => {
62 try {
63 if (!globalThis.crypto || !globalThis.crypto.subtle) { postMessage({ error: 'nocrypto' }); return; }
64 const { nonce, difficulty } = e.data;
65 const solution = await solve(b64urlDecode(nonce), difficulty, { onProgress: (tried) => { postMessage({ progress: tried }); } });
66 postMessage({ solution });
67 } catch (err) {
68 postMessage({ error: String(err && err.message || err) });
69 }
70 };
71 }
72})();