Skip to content

Commit 16867ad

Browse files
feat: add pure js pow module as fallback (#27)
Co-authored-by: LightQuantum <self@lightquantum.me>
1 parent fc0ec54 commit 16867ad

9 files changed

Lines changed: 296 additions & 37 deletions

File tree

translations/en.yaml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ en:
44
calculating: "Performing browser checks..."
55
difficulty_speed: "Difficulty: %{difficulty}, Speed: %{speed}kH/s"
66
taking_longer: "This is taking longer than expected. Please do not refresh the page."
7+
wasm_unavailable: "Your browser does not support WebAssembly. Computation may be slower."
78
why_seeing: "Why am I seeing this?"
89
why_seeing_body:
910
part_1: >-
@@ -21,10 +22,6 @@ en:
2122
server_error: "Server returned an error that we cannot handle."
2223
client_error: "Unexpected error occurred during verification."
2324
access_restricted: "Access has been restricted."
24-
must_enable_wasm: "Please enable WebAssembly to proceed."
25-
apologize_please_enable_wasm: >-
26-
Your browser has WebAssembly disabled via settings or an extension. It's known that some browsers (e.g. Safari with Lockdown Mode) disable WebAssembly by default.
27-
We apologize for the inconvenience, but please re-enable WebAssembly to proceed.
2825
browser_config_or_bug: "There might be an issue with your browser configuration, or something is wrong on our side. Please attach the error details when contacting us."
2926
error_details: "Error details: %{error}"
3027
ip_blocked: "You (or your local network) have been blocked due to suspicious activity."

translations/ko.yaml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ ko:
44
calculating: "브라우저 확인 중..."
55
difficulty_speed: "난이도: %{difficulty}, 속도: %{speed}kH/s"
66
taking_longer: "예상보다 시간이 오래 걸리고 있습니다. 페이지를 새로 고침하지 마세요."
7+
wasm_unavailable: "브라우저가 WebAssembly를 지원하지 않습니다. 연산 속도가 느려질 수 있습니다."
78
why_seeing: "이 페이지가 표시되는 이유는 무엇인가요?"
89
why_seeing_body:
910
part_1: >-
@@ -21,10 +22,6 @@ ko:
2122
server_error: "서버에서 처리할 수 없는 오류를 반환했습니다."
2223
client_error: "검증 중 예상치 못한 오류가 발생했습니다."
2324
access_restricted: "접근이 제한되었습니다."
24-
must_enable_wasm: "계속하려면 WebAssembly를 활성화해 주세요."
25-
apologize_please_enable_wasm: >-
26-
브라우저 설정이나 확장 프로그램으로 인해 WebAssembly가 비활성화되어 있습니다. 일부 브라우저(예: 잠금 모드를 사용하는 Safari)는 기본적으로 WebAssembly를 비활성화하는 것으로 알려져 있습니다.
27-
불편을 드려 죄송하지만, 계속하려면 WebAssembly를 다시 활성화해 주십시오.
2825
browser_config_or_bug: "브라우저 구성 문제이거나 서버 측의 문제일 수 있습니다. 문의 시 오류 세부 정보를 첨부해 주세요."
2926
error_details: "오류 세부 정보: %{error}"
3027
ip_blocked: "의심스러운 활동으로 인해 귀하(또는 귀하의 로컬 네트워크)가 차단되었습니다."

translations/zh.yaml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ zh:
55
calculating: "正在进行浏览器检查..."
66
difficulty_speed: "难度:%{difficulty},速度:%{speed}kH/s"
77
taking_longer: "验证时间超出预期,请勿刷新页面"
8+
wasm_unavailable: "您的浏览器不支持 WebAssembly,运算速度可能变慢"
89
why_seeing: "为什么我会看到这个页面?"
910
why_seeing_body:
1011
part_1: >-
@@ -22,10 +23,6 @@ zh:
2223
server_error: "服务器返回了未知错误"
2324
client_error: "验证过程中发生了意外错误"
2425
access_restricted: "访问受限"
25-
must_enable_wasm: "请启用 WebAssembly 以继续访问"
26-
apologize_please_enable_wasm: >-
27-
您的浏览器关闭了 WebAssembly,这可能是由于设置或插件导致的。已知部分浏览器(如 Safari 的锁定模式)会默认禁用 WebAssembly。
28-
您需要重新启用 JavaScript 才能继续访问,抱歉给您带来不便。
2926
browser_config_or_bug: "这可能是浏览器配置问题造成的,或是我们的系统出现了异常。联系我们时烦请您附上错误详情。"
3027
error_details: "错误详情:%{error}"
3128
ip_blocked: "由于检测到可疑活动,您的 IP 地址或本地网络已被封禁"

web/js/blake3.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Pure JavaScript implementation of BLAKE3 compression (single-chunk, truncated to 8 words)
2+
// Ported from pow/src/blake3.rs
3+
4+
export const IV = [
5+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
6+
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
7+
];
8+
9+
export const FLAG_CHUNK_START = 0x01;
10+
export const FLAG_CHUNK_END = 0x02;
11+
export const FLAG_ROOT = 0x08;
12+
13+
const PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8];
14+
15+
// Precompute message schedule (7 rounds)
16+
const MESSAGE_SCHEDULE = (() => {
17+
const out = [];
18+
let ix = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
19+
out.push(ix.slice());
20+
for (let i = 1; i < 7; i++) {
21+
const newIx = new Array(16);
22+
for (let j = 0; j < 16; j++) newIx[j] = ix[PERMUTATION[j]];
23+
ix = newIx;
24+
out.push(newIx.slice());
25+
}
26+
return out;
27+
})();
28+
29+
function g(state, a, b, c, d, mx, my) {
30+
state[a] = (state[a] + state[b] + mx) | 0;
31+
state[d] = ror32(state[d] ^ state[a], 16);
32+
state[c] = (state[c] + state[d]) | 0;
33+
state[b] = ror32(state[b] ^ state[c], 12);
34+
state[a] = (state[a] + state[b] + my) | 0;
35+
state[d] = ror32(state[d] ^ state[a], 8);
36+
state[c] = (state[c] + state[d]) | 0;
37+
state[b] = ror32(state[b] ^ state[c], 7);
38+
}
39+
40+
function ror32(v, n) {
41+
return (v >>> n) | (v << (32 - n));
42+
}
43+
44+
function roundFixed(state, m, round) {
45+
const s = MESSAGE_SCHEDULE[round];
46+
// Mix columns
47+
g(state, 0, 4, 8, 12, m[s[0]], m[s[1]]);
48+
g(state, 1, 5, 9, 13, m[s[2]], m[s[3]]);
49+
g(state, 2, 6, 10, 14, m[s[4]], m[s[5]]);
50+
g(state, 3, 7, 11, 15, m[s[6]], m[s[7]]);
51+
// Mix diagonals
52+
g(state, 0, 5, 10, 15, m[s[8]], m[s[9]]);
53+
g(state, 1, 6, 11, 12, m[s[10]], m[s[11]]);
54+
g(state, 2, 7, 8, 13, m[s[12]], m[s[13]]);
55+
g(state, 3, 4, 9, 14, m[s[14]], m[s[15]]);
56+
}
57+
58+
/**
59+
* Truncated BLAKE3 compression function (returns first 8 words).
60+
* @param {number[]} chainingValue - 8 u32 words
61+
* @param {number[]} blockWords - 16 u32 words
62+
* @param {number} counter - 64-bit counter (as Number, only low 32 bits used here)
63+
* @param {number} blockLen - block length in bytes
64+
* @param {number} flags - BLAKE3 flags
65+
* @returns {number[]} 8 u32 words
66+
*/
67+
export function compress8(chainingValue, blockWords, counter, blockLen, flags) {
68+
const counterLow = counter | 0;
69+
const counterHigh = 0; // counter fits in 32 bits for our use case
70+
const state = [
71+
chainingValue[0], chainingValue[1], chainingValue[2], chainingValue[3],
72+
chainingValue[4], chainingValue[5], chainingValue[6], chainingValue[7],
73+
IV[0], IV[1], IV[2], IV[3],
74+
counterLow, counterHigh, blockLen, flags,
75+
];
76+
77+
for (let i = 0; i < 7; i++) {
78+
roundFixed(state, blockWords, i);
79+
}
80+
81+
for (let i = 0; i < 8; i++) {
82+
state[i] ^= state[i + 8];
83+
}
84+
85+
return state.slice(0, 8);
86+
}
87+
88+
/**
89+
* Hash arbitrary data using BLAKE3 (single-chunk, up to 1024 bytes).
90+
* Returns 32-byte Uint8Array.
91+
* @param {Uint8Array} data
92+
* @returns {Uint8Array}
93+
*/
94+
export function blake3Hash(data) {
95+
let chainingValue = IV.slice();
96+
97+
// Process 64-byte blocks (at least 1 block even for empty input)
98+
const numBlocks = Math.max(1, Math.ceil(data.length / 64));
99+
for (let blockIdx = 0; blockIdx < numBlocks; blockIdx++) {
100+
const offset = blockIdx * 64;
101+
const remaining = Math.max(0, data.length - offset);
102+
const thisBlockLen = Math.min(64, remaining);
103+
104+
// Convert block bytes to 16 u32 words (LE)
105+
const block = new Array(16).fill(0);
106+
for (let i = 0; i < thisBlockLen; i++) {
107+
block[i >> 2] |= data[offset + i] << ((i & 3) * 8);
108+
}
109+
110+
let flags = 0;
111+
if (blockIdx === 0) flags |= FLAG_CHUNK_START;
112+
if (blockIdx === numBlocks - 1) flags |= FLAG_CHUNK_END | FLAG_ROOT;
113+
114+
chainingValue = compress8(chainingValue, block, 0, thisBlockLen, flags);
115+
}
116+
117+
// Convert u32 words to bytes (LE)
118+
const out = new Uint8Array(32);
119+
for (let i = 0; i < 8; i++) {
120+
out[i * 4] = chainingValue[i] & 0xff;
121+
out[i * 4 + 1] = (chainingValue[i] >>> 8) & 0xff;
122+
out[i * 4 + 2] = (chainingValue[i] >>> 16) & 0xff;
123+
out[i * 4 + 3] = (chainingValue[i] >>> 24) & 0xff;
124+
}
125+
return out;
126+
}
127+
128+
/**
129+
* Encode u32[8] hash to hex string (LE byte order, matching Rust encode_hex_le).
130+
* @param {number[]} hash - 8 u32 words
131+
* @returns {string}
132+
*/
133+
export function encodeHexLE(hash) {
134+
const hex = '0123456789abcdef';
135+
let out = '';
136+
for (let w = 0; w < 8; w++) {
137+
for (let i = 0; i < 4; i++) {
138+
const b = (hash[w] >>> (i * 8)) & 0xff;
139+
out += hex[b >> 4];
140+
out += hex[b & 0xf];
141+
}
142+
}
143+
return out;
144+
}
145+
146+
/**
147+
* Compute the difficulty mask (matching Rust compute_mask_cerberus).
148+
* @param {number} difficulty
149+
* @returns {number}
150+
*/
151+
export function computeMask(difficulty) {
152+
if (difficulty === 16) return ~0;
153+
// !(!0u32 >> (difficulty * 2)).swap_bytes()
154+
const shifted = (~0 >>> (difficulty * 2)) | 0;
155+
const swapped = byteSwap32(shifted);
156+
return (~swapped) | 0;
157+
}
158+
159+
function byteSwap32(v) {
160+
return (
161+
((v & 0xff) << 24) |
162+
(((v >>> 8) & 0xff) << 16) |
163+
(((v >>> 16) & 0xff) << 8) |
164+
((v >>> 24) & 0xff)
165+
) | 0;
166+
}

web/js/main.mjs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,9 @@ const handleError = (error) => {
8888
ui.title(t('error.error_occurred'));
8989
ui.mascotState('fail');
9090

91-
if (error.message && error.message.includes("Failed to initialize WebAssembly module")) {
92-
ui.message(t('error.must_enable_wasm'));
93-
ui.description(t('error.apologize_please_enable_wasm'));
94-
console.error(error);
95-
} else {
96-
ui.message(t('error.client_error'));
97-
ui.description(t('error.browser_config_or_bug'));
98-
ui.code(t('error.error_details', { error: error.message }));
99-
}
91+
ui.message(t('error.client_error'));
92+
ui.description(t('error.browser_config_or_bug'));
93+
ui.code(t('error.error_details', { error: error.message }));
10094
}
10195

10296
const main = async () => {
@@ -143,9 +137,13 @@ const main = async () => {
143137
const speed = totalIters / delta;
144138
ui.progress(distance);
145139
ui.metrics(t('challenge.difficulty_speed', { difficulty, speed: speed.toFixed(3) }));
146-
ui.progressMessage(probability < 0.01 ? t('challenge.taking_longer') : undefined);
140+
if (probability < 0.01) {
141+
ui.progressMessage(t('challenge.taking_longer'));
142+
}
147143
lastUpdate = delta;
148144
};
145+
}, () => {
146+
ui.progressMessage(t('challenge.wasm_unavailable'));
149147
});
150148
const t1 = Date.now();
151149

@@ -164,4 +162,4 @@ const main = async () => {
164162

165163
};
166164

167-
main().catch(handleError);
165+
main().catch(handleError);

web/js/pow.js.worker.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Pure JS PoW worker (fallback when WebAssembly is unavailable)
2+
import { compress8, blake3Hash, encodeHexLE, computeMask, IV, FLAG_CHUNK_START, FLAG_CHUNK_END, FLAG_ROOT } from './blake3.js';
3+
4+
const REPORT_PERIOD = 16384;
5+
6+
addEventListener('message', (event) => {
7+
const { data, difficulty, nonce: threadId, threads } = event.data;
8+
9+
const mask = computeMask(difficulty);
10+
const reportSlot = (threadId * REPORT_PERIOD / threads) | 0;
11+
12+
// Compute salt = blake3(data) as hex bytes
13+
const encoder = new TextEncoder();
14+
const dataBytes = encoder.encode(data);
15+
const saltBytes = blake3Hash(dataBytes);
16+
17+
// Convert salt (32 bytes) to 64-char hex, then to 64 bytes of ASCII
18+
const hex = '0123456789abcdef';
19+
const saltHex = new Uint8Array(64);
20+
for (let i = 0; i < 32; i++) {
21+
saltHex[i * 2] = hex.charCodeAt(saltBytes[i] >> 4);
22+
saltHex[i * 2 + 1] = hex.charCodeAt(saltBytes[i] & 0xf);
23+
}
24+
25+
// Compute midstate: compress(IV, saltHex as u32[16], counter=0, blockLen=64, FLAG_CHUNK_START)
26+
const initBlock = new Array(16);
27+
for (let i = 0; i < 16; i++) {
28+
initBlock[i] = saltHex[i * 4] |
29+
(saltHex[i * 4 + 1] << 8) |
30+
(saltHex[i * 4 + 2] << 16) |
31+
(saltHex[i * 4 + 3] << 24);
32+
}
33+
const midstate = compress8(IV, initBlock, 0, 64, FLAG_CHUNK_START);
34+
35+
let set = threadId;
36+
const trailingFlags = FLAG_CHUNK_END | FLAG_ROOT;
37+
38+
while (true) {
39+
const msg = new Array(16).fill(0);
40+
msg[0] = set;
41+
let attemptedNonces = 0;
42+
43+
for (let nonce = 0; nonce < 0xFFFFFFFF; nonce++) {
44+
msg[1] = nonce;
45+
46+
const hash = compress8(midstate, msg, 0, 8, trailingFlags);
47+
attemptedNonces++;
48+
49+
if (attemptedNonces % REPORT_PERIOD === reportSlot) {
50+
postMessage(REPORT_PERIOD);
51+
}
52+
53+
if ((hash[0] & mask) === 0) {
54+
const hashHex = encodeHexLE(hash);
55+
// solution = nonce as u64 | (batchId as u64) << 32
56+
const solution = nonce + set * 0x100000000;
57+
postMessage({
58+
hash: hashHex,
59+
difficulty,
60+
nonce: solution,
61+
});
62+
return;
63+
}
64+
}
65+
66+
// Exhausted nonce space for this batch_id, try next
67+
set += threads;
68+
if (set > 0xFFFFFFFF) return;
69+
}
70+
});

0 commit comments

Comments
 (0)