From cc1d5b71da0cbc79c3f84a2f59d3daa38d1a06e6 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Wed, 9 Apr 2025 00:12:38 -0400 Subject: experiment: start implementing checks in wasm (client side only so far) Signed-off-by: Xe Iaso --- web/js/algos/fast.mjs | 130 ++++++++++++++++++++++++++++++++++ web/js/algos/sha256.mjs | 160 ++++++++++++++++++++++++++++++++++++++++++ web/js/algos/slow.mjs | 89 +++++++++++++++++++++++ web/js/bench.mjs | 17 +++-- web/js/main.mjs | 23 +++--- web/js/proof-of-work-slow.mjs | 90 ------------------------ web/js/proof-of-work.mjs | 132 ---------------------------------- web/js/xeact.mjs | 13 ++++ web/static/wasm/.gitignore | 2 + 9 files changed, 417 insertions(+), 239 deletions(-) create mode 100644 web/js/algos/fast.mjs create mode 100644 web/js/algos/sha256.mjs create mode 100644 web/js/algos/slow.mjs delete mode 100644 web/js/proof-of-work-slow.mjs delete mode 100644 web/js/proof-of-work.mjs create mode 100644 web/js/xeact.mjs create mode 100644 web/static/wasm/.gitignore (limited to 'web') diff --git a/web/js/algos/fast.mjs b/web/js/algos/fast.mjs new file mode 100644 index 0000000..a077c22 --- /dev/null +++ b/web/js/algos/fast.mjs @@ -0,0 +1,130 @@ +export default function process( + data, + difficulty = 5, + signal = null, + progressCallback = null, + threads = (navigator.hardwareConcurrency || 1), +) { + return new Promise((resolve, reject) => { + let webWorkerURL = URL.createObjectURL(new Blob([ + '(', processTask(), ')()' + ], { type: 'application/javascript' })); + + const workers = []; + const terminate = () => { + workers.forEach((w) => w.terminate()); + if (signal != null) { + // clean up listener to avoid memory leak + signal.removeEventListener("abort", terminate); + if (signal.aborted) { + console.log("PoW aborted"); + reject(false); + } + } + }; + if (signal != null) { + signal.addEventListener("abort", terminate, { once: true }); + } + + for (let i = 0; i < threads; i++) { + let worker = new Worker(webWorkerURL); + + worker.onmessage = (event) => { + if (typeof event.data === "number") { + progressCallback?.(event.data); + } else { + terminate(); + resolve(event.data); + } + }; + + worker.onerror = (event) => { + terminate(); + reject(event); + }; + + worker.postMessage({ + data, + difficulty, + nonce: i, + threads, + }); + + workers.push(worker); + } + + URL.revokeObjectURL(webWorkerURL); + }); +} + +function processTask() { + return function () { + const sha256 = (text) => { + const encoded = new TextEncoder().encode(text); + return crypto.subtle.digest("SHA-256", encoded.buffer); + }; + + function uint8ArrayToHexString(arr) { + return Array.from(arr) + .map((c) => c.toString(16).padStart(2, "0")) + .join(""); + } + + addEventListener('message', async (event) => { + let data = event.data.data; + let difficulty = event.data.difficulty; + let hash; + let nonce = event.data.nonce; + let threads = event.data.threads; + + const threadId = nonce; + + while (true) { + const currentHash = await sha256(data + nonce); + const thisHash = new Uint8Array(currentHash); + let valid = true; + + for (let j = 0; j < difficulty; j++) { + const byteIndex = Math.floor(j / 2); // which byte we are looking at + const nibbleIndex = j % 2; // which nibble in the byte we are looking at (0 is high, 1 is low) + + let nibble = (thisHash[byteIndex] >> (nibbleIndex === 0 ? 4 : 0)) & 0x0F; // Get the nibble + + if (nibble !== 0) { + valid = false; + break; + } + } + + if (valid) { + hash = uint8ArrayToHexString(thisHash); + break; + } + + const oldNonce = nonce; + nonce += threads; + + // send a progress update every 1024 iterations. since each thread checks + // separate values, one simple way to do this is by bit masking the + // nonce for multiples of 1024. unfortunately, if the number of threads + // is not prime, only some of the threads will be sending the status + // update and they will get behind the others. this is slightly more + // complicated but ensures an even distribution between threads. + if ( + nonce > oldNonce | 1023 && // we've wrapped past 1024 + (nonce >> 10) % threads === threadId // and it's our turn + ) { + postMessage(nonce); + } + } + + postMessage({ + hash, + data, + difficulty, + nonce, + }); + }); + }.toString(); +} + diff --git a/web/js/algos/sha256.mjs b/web/js/algos/sha256.mjs new file mode 100644 index 0000000..cfea876 --- /dev/null +++ b/web/js/algos/sha256.mjs @@ -0,0 +1,160 @@ +import { u } from "../xeact.mjs"; + +export default function process( + data, + difficulty = 16, + signal = null, + pc = null, + threads = (navigator.hardwareConcurrency || 1), +) { + return new Promise(async (resolve, reject) => { + let webWorkerURL = URL.createObjectURL(new Blob([ + '(', processTask(), ')()' + ], { type: 'application/javascript' })); + + const module = await fetch(u("/.within.website/x/cmd/anubis/static/wasm/sha256.wasm")) + .then(resp => WebAssembly.compileStreaming(resp)); + + const workers = []; + const terminate = () => { + workers.forEach((w) => w.terminate()); + if (signal != null) { + // clean up listener to avoid memory leak + signal.removeEventListener("abort", terminate); + if (signal.aborted) { + console.log("PoW aborted"); + reject(false); + } + } + }; + if (signal != null) { + signal.addEventListener("abort", terminate, { once: true }); + } + + for (let i = 0; i < threads; i++) { + let worker = new Worker(webWorkerURL); + + worker.onmessage = (event) => { + if (typeof event.data === "number") { + pc?.(event.data); + } else { + terminate(); + resolve(event.data); + } + }; + + worker.onerror = (event) => { + terminate(); + reject(event); + }; + + worker.postMessage({ + data, + difficulty, + nonce: i, + threads, + module, + }); + + workers.push(worker); + } + + URL.revokeObjectURL(webWorkerURL); + }); +} + +function processTask() { + return function () { + addEventListener('message', async (event) => { + const importObject = { + anubis: { + anubis_update_nonce: (nonce) => postMessage(nonce), + } + }; + + const instance = await WebAssembly.instantiate(event.data.module, importObject); + + // Get exports + const { + anubis_work, + data_ptr, + result_hash_ptr, + result_hash_size, + set_data_length, + memory + } = instance.exports; + + function uint8ArrayToHex(arr) { + return Array.from(arr) + .map((c) => c.toString(16).padStart(2, "0")) + .join(""); + } + + function hexToUint8Array(hexString) { + // Remove whitespace and optional '0x' prefix + hexString = hexString.replace(/\s+/g, '').replace(/^0x/, ''); + + // Check for valid length + if (hexString.length % 2 !== 0) { + throw new Error('Invalid hex string length'); + } + + // Check for valid characters + if (!/^[0-9a-fA-F]+$/.test(hexString)) { + throw new Error('Invalid hex characters'); + } + + // Convert to Uint8Array + const byteArray = new Uint8Array(hexString.length / 2); + for (let i = 0; i < byteArray.length; i++) { + const byteValue = parseInt(hexString.substr(i * 2, 2), 16); + byteArray[i] = byteValue; + } + + return byteArray; + } + + // Write data to buffer + function writeToBuffer(data) { + if (data.length > 1024) throw new Error("Data exceeds buffer size"); + + // Get pointer and create view + const offset = data_ptr(); + const buffer = new Uint8Array(memory.buffer, offset, data.length); + + // Copy data + buffer.set(data); + + // Set data length + set_data_length(data.length); + } + + function readFromChallenge() { + const offset = result_hash_ptr(); + const buffer = new Uint8Array(memory.buffer, offset, result_hash_size()); + + return buffer; + } + + let data = event.data.data; + let difficulty = event.data.difficulty; + let hash; + let nonce = event.data.nonce; + let interand = event.data.threads; + + writeToBuffer(hexToUint8Array(data)); + + nonce = anubis_work(difficulty, nonce, interand); + const challenge = readFromChallenge(); + + data = uint8ArrayToHex(challenge); + + postMessage({ + hash: data, + difficulty, + nonce, + }); + }); + }.toString(); +} + diff --git a/web/js/algos/slow.mjs b/web/js/algos/slow.mjs new file mode 100644 index 0000000..2304859 --- /dev/null +++ b/web/js/algos/slow.mjs @@ -0,0 +1,89 @@ +// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm + +export default function process( + data, + difficulty = 5, + signal = null, + progressCallback = null, + _threads = 1, +) { + return new Promise((resolve, reject) => { + let webWorkerURL = URL.createObjectURL(new Blob([ + '(', processTask(), ')()' + ], { type: 'application/javascript' })); + + let worker = new Worker(webWorkerURL); + const terminate = () => { + worker.terminate(); + if (signal != null) { + // clean up listener to avoid memory leak + signal.removeEventListener("abort", terminate); + if (signal.aborted) { + console.log("PoW aborted"); + reject(false); + } + } + }; + if (signal != null) { + signal.addEventListener("abort", terminate, { once: true }); + } + + worker.onmessage = (event) => { + if (typeof event.data === "number") { + progressCallback?.(event.data); + } else { + terminate(); + resolve(event.data); + } + }; + + worker.onerror = (event) => { + terminate(); + reject(event); + }; + + worker.postMessage({ + data, + difficulty + }); + + URL.revokeObjectURL(webWorkerURL); + }); +} + +function processTask() { + return function () { + const sha256 = (text) => { + const encoded = new TextEncoder().encode(text); + return crypto.subtle.digest("SHA-256", encoded.buffer) + .then((result) => + Array.from(new Uint8Array(result)) + .map((c) => c.toString(16).padStart(2, "0")) + .join(""), + ); + }; + + addEventListener('message', async (event) => { + let data = event.data.data; + let difficulty = event.data.difficulty; + + let hash; + let nonce = 0; + do { + if (nonce & 1023 === 0) { + postMessage(nonce); + } + hash = await sha256(data + nonce++); + } while (hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')); + + nonce -= 1; // last nonce was post-incremented + + postMessage({ + hash, + data, + difficulty, + nonce, + }); + }); + }.toString(); +} \ No newline at end of file diff --git a/web/js/bench.mjs b/web/js/bench.mjs index c8c69bd..93daa90 100644 --- a/web/js/bench.mjs +++ b/web/js/bench.mjs @@ -1,10 +1,12 @@ -import processFast from "./proof-of-work.mjs"; -import processSlow from "./proof-of-work-slow.mjs"; +import fast from "./algos/fast.mjs"; +import slow from "./algos/slow.mjs"; +import sha256 from "./algos/sha256.mjs"; -const defaultDifficulty = 4; +const defaultDifficulty = 16; const algorithms = { - fast: processFast, - slow: processSlow, + sha256: sha256, + fast: fast, + slow: slow, }; const status = document.getElementById("status"); @@ -41,10 +43,13 @@ const benchmarkTrial = async (stats, difficulty, algorithm, signal) => { .map((c) => c.toString(16).padStart(2, "0")) .join(""); + if (algorithm != "sha256") { + difficulty = Math.round(difficulty / 4); + } + const t0 = performance.now(); const { hash, nonce } = await process(challenge, Number(difficulty), signal); const t1 = performance.now(); - console.log({ hash, nonce }); stats.time += t1 - t0; stats.iters += nonce; diff --git a/web/js/main.mjs b/web/js/main.mjs index a093c74..79d62e9 100644 --- a/web/js/main.mjs +++ b/web/js/main.mjs @@ -1,17 +1,13 @@ -import processFast from "./proof-of-work.mjs"; -import processSlow from "./proof-of-work-slow.mjs"; +import fast from "./algos/fast.mjs"; +import slow from "./algos/slow.mjs"; +import sha256 from "./algos/sha256.mjs"; import { testVideo } from "./video.mjs"; +import { u } from "./xeact.mjs"; const algorithms = { - "fast": processFast, - "slow": processSlow, -}; - -// from Xeact -const u = (url = "", params = {}) => { - let result = new URL(url, window.location.href); - Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v)); - return result.toString(); + "fast": fast, + "slow": slow, + "sha256": sha256, }; const imageURL = (mood, cacheBuster) => @@ -28,6 +24,11 @@ const dependencies = [ msg: "Your browser doesn't support web workers (Anubis uses this to avoid freezing your browser). Do you have a plugin like JShelter installed?", value: window.Worker, }, + { + name: "WebAssembly", + msg: "Your browser doesn't have WebAssembly support. If you are running a big endian system, I'm sorry but this is something we can't work around with a polyfill.", + value: window.WebAssembly, + }, ]; function showContinueBar(hash, nonce, t0, t1) { diff --git a/web/js/proof-of-work-slow.mjs b/web/js/proof-of-work-slow.mjs deleted file mode 100644 index 0bdc146..0000000 --- a/web/js/proof-of-work-slow.mjs +++ /dev/null @@ -1,90 +0,0 @@ -// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm - -export default function process( - data, - difficulty = 5, - signal = null, - progressCallback = null, - _threads = 1, -) { - console.debug("slow algo"); - return new Promise((resolve, reject) => { - let webWorkerURL = URL.createObjectURL(new Blob([ - '(', processTask(), ')()' - ], { type: 'application/javascript' })); - - let worker = new Worker(webWorkerURL); - const terminate = () => { - worker.terminate(); - if (signal != null) { - // clean up listener to avoid memory leak - signal.removeEventListener("abort", terminate); - if (signal.aborted) { - console.log("PoW aborted"); - reject(false); - } - } - }; - if (signal != null) { - signal.addEventListener("abort", terminate, { once: true }); - } - - worker.onmessage = (event) => { - if (typeof event.data === "number") { - progressCallback?.(event.data); - } else { - terminate(); - resolve(event.data); - } - }; - - worker.onerror = (event) => { - terminate(); - reject(event); - }; - - worker.postMessage({ - data, - difficulty - }); - - URL.revokeObjectURL(webWorkerURL); - }); -} - -function processTask() { - return function () { - const sha256 = (text) => { - const encoded = new TextEncoder().encode(text); - return crypto.subtle.digest("SHA-256", encoded.buffer) - .then((result) => - Array.from(new Uint8Array(result)) - .map((c) => c.toString(16).padStart(2, "0")) - .join(""), - ); - }; - - addEventListener('message', async (event) => { - let data = event.data.data; - let difficulty = event.data.difficulty; - - let hash; - let nonce = 0; - do { - if (nonce & 1023 === 0) { - postMessage(nonce); - } - hash = await sha256(data + nonce++); - } while (hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')); - - nonce -= 1; // last nonce was post-incremented - - postMessage({ - hash, - data, - difficulty, - nonce, - }); - }); - }.toString(); -} \ No newline at end of file diff --git a/web/js/proof-of-work.mjs b/web/js/proof-of-work.mjs deleted file mode 100644 index 5ef3a8a..0000000 --- a/web/js/proof-of-work.mjs +++ /dev/null @@ -1,132 +0,0 @@ -export default function process( - data, - difficulty = 5, - signal = null, - progressCallback = null, - threads = (navigator.hardwareConcurrency || 1), -) { - console.debug("fast algo"); - return new Promise((resolve, reject) => { - let webWorkerURL = URL.createObjectURL(new Blob([ - '(', processTask(), ')()' - ], { type: 'application/javascript' })); - - const workers = []; - const terminate = () => { - workers.forEach((w) => w.terminate()); - if (signal != null) { - // clean up listener to avoid memory leak - signal.removeEventListener("abort", terminate); - if (signal.aborted) { - console.log("PoW aborted"); - reject(false); - } - } - }; - if (signal != null) { - signal.addEventListener("abort", terminate, { once: true }); - } - - for (let i = 0; i < threads; i++) { - let worker = new Worker(webWorkerURL); - - worker.onmessage = (event) => { - if (typeof event.data === "number") { - progressCallback?.(event.data); - } else { - terminate(); - resolve(event.data); - } - }; - - worker.onerror = (event) => { - terminate(); - reject(event); - }; - - worker.postMessage({ - data, - difficulty, - nonce: i, - threads, - }); - - workers.push(worker); - } - - URL.revokeObjectURL(webWorkerURL); - }); -} - -function processTask() { - return function () { - const sha256 = (text) => { - const encoded = new TextEncoder().encode(text); - return crypto.subtle.digest("SHA-256", encoded.buffer); - }; - - function uint8ArrayToHexString(arr) { - return Array.from(arr) - .map((c) => c.toString(16).padStart(2, "0")) - .join(""); - } - - addEventListener('message', async (event) => { - let data = event.data.data; - let difficulty = event.data.difficulty; - let hash; - let nonce = event.data.nonce; - let threads = event.data.threads; - - const threadId = nonce; - - while (true) { - const currentHash = await sha256(data + nonce); - const thisHash = new Uint8Array(currentHash); - let valid = true; - - for (let j = 0; j < difficulty; j++) { - const byteIndex = Math.floor(j / 2); // which byte we are looking at - const nibbleIndex = j % 2; // which nibble in the byte we are looking at (0 is high, 1 is low) - - let nibble = (thisHash[byteIndex] >> (nibbleIndex === 0 ? 4 : 0)) & 0x0F; // Get the nibble - - if (nibble !== 0) { - valid = false; - break; - } - } - - if (valid) { - hash = uint8ArrayToHexString(thisHash); - console.log(hash); - break; - } - - const oldNonce = nonce; - nonce += threads; - - // send a progress update every 1024 iterations. since each thread checks - // separate values, one simple way to do this is by bit masking the - // nonce for multiples of 1024. unfortunately, if the number of threads - // is not prime, only some of the threads will be sending the status - // update and they will get behind the others. this is slightly more - // complicated but ensures an even distribution between threads. - if ( - nonce > oldNonce | 1023 && // we've wrapped past 1024 - (nonce >> 10) % threads === threadId // and it's our turn - ) { - postMessage(nonce); - } - } - - postMessage({ - hash, - data, - difficulty, - nonce, - }); - }); - }.toString(); -} - diff --git a/web/js/xeact.mjs b/web/js/xeact.mjs new file mode 100644 index 0000000..5d5e8e3 --- /dev/null +++ b/web/js/xeact.mjs @@ -0,0 +1,13 @@ +/** + * Generate a relative URL from `url`, appending all key-value pairs from `params` as URL-encoded parameters. + * + * @type{function(string=, Object=): string} + */ +export const u = (url = "", params = {}) => { + let result = new URL(url, window.location.href); + Object.entries(params).forEach((kv) => { + let [k, v] = kv; + result.searchParams.set(k, v); + }); + return result.toString(); +}; \ No newline at end of file diff --git a/web/static/wasm/.gitignore b/web/static/wasm/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/web/static/wasm/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file -- cgit v1.2.3