From cc1d5b71da0cbc79c3f84a2f59d3daa38d1a06e6 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Wed, 9 Apr 2025 00:12:38 -0400 Subject: [PATCH] experiment: start implementing checks in wasm (client side only so far) Signed-off-by: Xe Iaso --- .gitignore | 4 + Cargo.lock | 100 +++++++++++ Cargo.toml | 9 + wasm/pow/sha256/Cargo.toml | 20 +++ wasm/pow/sha256/run.html | 1 + wasm/pow/sha256/run.js | 105 ++++++++++++ wasm/pow/sha256/src/lib.rs | 139 +++++++++++++++ web/js/{proof-of-work.mjs => algos/fast.mjs} | 2 - web/js/algos/sha256.mjs | 160 ++++++++++++++++++ .../slow.mjs} | 1 - web/js/bench.mjs | 17 +- web/js/main.mjs | 23 +-- web/js/xeact.mjs | 13 ++ web/static/wasm/.gitignore | 2 + 14 files changed, 576 insertions(+), 20 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 wasm/pow/sha256/Cargo.toml create mode 100644 wasm/pow/sha256/run.html create mode 100644 wasm/pow/sha256/run.js create mode 100644 wasm/pow/sha256/src/lib.rs rename web/js/{proof-of-work.mjs => algos/fast.mjs} (98%) create mode 100644 web/js/algos/sha256.mjs rename web/js/{proof-of-work-slow.mjs => algos/slow.mjs} (98%) create mode 100644 web/js/xeact.mjs create mode 100644 web/static/wasm/.gitignore diff --git a/.gitignore b/.gitignore index a716c66..4accc2b 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ node_modules # how does this get here doc/VERSION + +*.wasm + +target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..57bdde7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,100 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha256" +version = "0.1.0" +dependencies = [ + "lazy_static", + "sha2", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9e5a7ac --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] +resolver = "2" +members = ["wasm/pow/*"] + +[profile.release] +strip = true +opt-level = "s" +lto = "thin" +codegen-units = 1 diff --git a/wasm/pow/sha256/Cargo.toml b/wasm/pow/sha256/Cargo.toml new file mode 100644 index 0000000..c2bf025 --- /dev/null +++ b/wasm/pow/sha256/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "sha256" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +lazy_static = "1.5" +sha2 = "0.10" + +[lints.clippy] +nursery = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +unwrap_used = "warn" +uninlined_format_args = "allow" +missing_panics_doc = "allow" +missing_errors_doc = "allow" +cognitive_complexity = "allow" diff --git a/wasm/pow/sha256/run.html b/wasm/pow/sha256/run.html new file mode 100644 index 0000000..2e6f235 --- /dev/null +++ b/wasm/pow/sha256/run.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/wasm/pow/sha256/run.js b/wasm/pow/sha256/run.js new file mode 100644 index 0000000..8f9c5e0 --- /dev/null +++ b/wasm/pow/sha256/run.js @@ -0,0 +1,105 @@ +// Load and instantiate the .wasm file +const response = await fetch("sha256.wasm"); + +const importObject = { + anubis: { + anubis_update_nonce: (nonce) => { + console.log(`Received nonce update: ${nonce}`); + // Your logic here + } + } +}; + +const module = await WebAssembly.compileStreaming(response); +const instance = await WebAssembly.instantiate(module, importObject); + +// Get exports +const { + anubis_work, + anubis_validate, + data_ptr, + result_hash_ptr, + result_hash_size, + verification_hash_ptr, + verification_hash_size, + set_data_length, + memory +} = instance.exports; + +console.log(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; +} + +// Example usage: +const data = hexToUint8Array("98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4"); +writeToBuffer(data); + +// Call work function +const t0 = Date.now(); +const nonce = anubis_work(16, 0, 1); +const t1 = Date.now(); + +console.log(`Done! Took ${t1 - t0}ms, ${nonce} iterations`); + +const challengeBuffer = readFromChallenge(); + +{ + const buffer = new Uint8Array(memory.buffer, verification_hash_ptr(), verification_hash_size()); + buffer.set(challengeBuffer); +} + +// Validate +const isValid = anubis_validate(nonce, 10) === 1; +console.log(isValid); + +console.log(uint8ArrayToHex(readFromChallenge())); \ No newline at end of file diff --git a/wasm/pow/sha256/src/lib.rs b/wasm/pow/sha256/src/lib.rs new file mode 100644 index 0000000..fd53bdf --- /dev/null +++ b/wasm/pow/sha256/src/lib.rs @@ -0,0 +1,139 @@ +use lazy_static::lazy_static; +use sha2::{Digest, Sha256}; +use std::sync::Mutex; + +lazy_static! { + static ref DATA_BUFFER: Mutex<[u8; 1024]> = Mutex::new([0; 1024]); + static ref DATA_LENGTH: Mutex = Mutex::new(0); + static ref RESULT_HASH: Mutex<[u8; 32]> = Mutex::new([0; 32]); + static ref VERIFICATION_HASH: Mutex<[u8; 32]> = Mutex::new([0; 32]); +} + +#[link(wasm_import_module = "anubis")] // Usually matches your JS namespace +unsafe extern "C" { + // Declare the imported function + fn anubis_update_nonce(nonce: u32); +} + +fn update_nonce(nonce: u32) { + unsafe { + anubis_update_nonce(nonce); + } +} + +/// Core validation function +fn validate(hash: &[u8], difficulty: u32) -> bool { + let mut remaining = difficulty; + for &byte in hash { + if remaining == 0 { + break; + } + if remaining >= 8 { + if byte != 0 { + return false; + } + remaining -= 8; + } else { + let mask = 0xFF << (8 - remaining); + if (byte & mask) != 0 { + return false; + } + remaining = 0; + } + } + true +} + +/// Computes hash for given nonce +fn compute_hash(nonce: u32) -> [u8; 32] { + let data = DATA_BUFFER.lock().unwrap(); + let data_len = *DATA_LENGTH.lock().unwrap(); + let use_le = data[data_len - 1] >= 128; + + let data_slice = &data[..data_len]; + + let mut hasher = Sha256::new(); + hasher.update(data_slice); + hasher.update(if use_le { + nonce.to_le_bytes() + } else { + nonce.to_be_bytes() + }); + hasher.finalize().into() +} + +// WebAssembly exports + +#[unsafe(no_mangle)] +pub extern "C" fn anubis_work(difficulty: u32, initial_nonce: u32, iterand: u32) -> u32 { + let mut nonce = initial_nonce; + + loop { + let hash = compute_hash(nonce); + + if validate(&hash, difficulty) { + let mut challenge = RESULT_HASH.lock().unwrap(); + challenge.copy_from_slice(&hash); + return nonce; + } + + let old_nonce = nonce; + nonce = nonce.wrapping_add(iterand); + + // 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 > old_nonce | 1023 && (nonce >> 10) % iterand == initial_nonce { + update_nonce(nonce); + } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn anubis_validate(nonce: u32, difficulty: u32) -> bool { + let computed = compute_hash(nonce); + let valid = validate(&computed, difficulty); + + let verification = VERIFICATION_HASH.lock().unwrap(); + valid && computed == *verification +} + +// Memory accessors + +#[unsafe(no_mangle)] +pub extern "C" fn result_hash_ptr() -> *const u8 { + let challenge = RESULT_HASH.lock().unwrap(); + challenge.as_ptr() +} + +#[unsafe(no_mangle)] +pub extern "C" fn result_hash_size() -> usize { + RESULT_HASH.lock().unwrap().len() +} + +#[unsafe(no_mangle)] +pub extern "C" fn verification_hash_ptr() -> *const u8 { + let verification = VERIFICATION_HASH.lock().unwrap(); + verification.as_ptr() +} + +#[unsafe(no_mangle)] +pub extern "C" fn verification_hash_size() -> usize { + VERIFICATION_HASH.lock().unwrap().len() +} + +#[unsafe(no_mangle)] +pub extern "C" fn data_ptr() -> *const u8 { + let challenge = DATA_BUFFER.lock().unwrap(); + challenge.as_ptr() +} + +#[unsafe(no_mangle)] +pub extern "C" fn set_data_length(len: u32) { + // Add missing length setter + let mut data_length = DATA_LENGTH.lock().unwrap(); + *data_length = len as usize; +} diff --git a/web/js/proof-of-work.mjs b/web/js/algos/fast.mjs similarity index 98% rename from web/js/proof-of-work.mjs rename to web/js/algos/fast.mjs index 5ef3a8a..a077c22 100644 --- a/web/js/proof-of-work.mjs +++ b/web/js/algos/fast.mjs @@ -5,7 +5,6 @@ export default function process( progressCallback = null, threads = (navigator.hardwareConcurrency || 1), ) { - console.debug("fast algo"); return new Promise((resolve, reject) => { let webWorkerURL = URL.createObjectURL(new Blob([ '(', processTask(), ')()' @@ -99,7 +98,6 @@ function processTask() { if (valid) { hash = uint8ArrayToHexString(thisHash); - console.log(hash); break; } 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/proof-of-work-slow.mjs b/web/js/algos/slow.mjs similarity index 98% rename from web/js/proof-of-work-slow.mjs rename to web/js/algos/slow.mjs index 0bdc146..2304859 100644 --- a/web/js/proof-of-work-slow.mjs +++ b/web/js/algos/slow.mjs @@ -7,7 +7,6 @@ export default function process( progressCallback = null, _threads = 1, ) { - console.debug("slow algo"); return new Promise((resolve, reject) => { let webWorkerURL = URL.createObjectURL(new Blob([ '(', processTask(), ')()' 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/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