diff --git a/package-lock.json b/package-lock.json index 1659a143..ca24afb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", + "@haribala/wasm2js": "^1.1.1", "preact": "^10.27.2", "wasm-feature-detect": "^1.8.0" }, @@ -504,6 +505,12 @@ "node": ">=18" } }, + "node_modules/@haribala/wasm2js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@haribala/wasm2js/-/wasm2js-1.1.1.tgz", + "integrity": "sha512-PCZxbPNYJ3Nax1EPKb6oz2TSraSbhQyEXL2wZDlardsE7IwP6HHHVJVvDYWDz5p5HcASAvWRi74utGIepagTeA==", + "license": "Apache-2.0" + }, "node_modules/@smithy/is-array-buffer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", diff --git a/package.json b/package.json index c57cb444..ff20e4ef 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ }, "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", + "@haribala/wasm2js": "^1.1.1", "preact": "^10.27.2", "wasm-feature-detect": "^1.8.0" } diff --git a/web/js/algorithms/wasm.ts b/web/js/algorithms/wasm.ts index 2f4bb6bd..10c56a5f 100644 --- a/web/js/algorithms/wasm.ts +++ b/web/js/algorithms/wasm.ts @@ -1,7 +1,8 @@ import { u } from "../../lib/xeact"; import { simd } from "wasm-feature-detect"; +// import { compile } from '@haribala/wasm2js'; -type ProgressCallback = (nonce: number) => void; +type ProgressCallback = (nonce: number | string) => void; interface ProcessOptions { basePrefix: string; @@ -12,6 +13,102 @@ interface ProcessOptions { const getHardwareConcurrency = () => navigator.hardwareConcurrency !== undefined ? navigator.hardwareConcurrency : 1; +// // https://stackoverflow.com/questions/47879864/how-can-i-check-if-a-browser-supports-webassembly +// const isWASMSupported = (() => { +// try { +// if (typeof WebAssembly === "object" +// && typeof WebAssembly.instantiate === "function") { +// const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); +// if (module instanceof WebAssembly.Module) +// return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; +// } +// } catch (e) { +// return false; +// } +// return false; +// })(); + +// const fetchWASMBinary = async (url: string): Promise => { +// const res = await fetch(url); +// if (!res.ok) throw new Error('failed to fetch the wasm binary'); +// const wasmBytes = new Uint8Array(await res.arrayBuffer()); +// return wasmBytes; +// }; + +// const runPureJS = ( +// options: ProcessOptions, +// data: string, +// difficulty: number = 5, +// signal: AbortSignal | null = null, +// progressCallback?: ProgressCallback, +// threads: number = Math.trunc(Math.max(getHardwareConcurrency() / 2, 1)), +// ): Promise => { +// const { basePrefix, version, algorithm } = options; + +// return new Promise(async (resolve, reject) => { +// const module = await fetchWASMBinary(u(`${basePrefix}/.within.website/x/cmd/anubis/static/wasm/baseline/${algorithm}.wasm?cacheBuster=${version}`)); +// const jsCode = compile(module, true); +// progressCallback != undefined && progressCallback("Using pure JS mode, this will be slow"); + +// console.log(jsCode); + +// const webWorkerURL = `${options.basePrefix}/.within.website/x/cmd/anubis/static/js/worker/wasm.mjs?cacheBuster=${version}`; + +// const workers: Worker[] = []; +// let settled = false; + +// const onAbort = () => { +// console.log("PoW aborted"); +// cleanup(); +// reject(new DOMException("Aborted", "AbortError")); +// }; + +// const cleanup = () => { +// if (settled) { +// return; +// } +// settled = true; +// workers.forEach((w) => w.terminate()); +// if (signal != null) { +// signal.removeEventListener("abort", onAbort); +// } +// }; + +// if (signal != null) { +// if (signal.aborted) { +// return onAbort(); +// } +// signal.addEventListener("abort", onAbort, { 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 { +// cleanup(); +// resolve(event.data); +// } +// } + +// worker.onerror = (event) => { +// cleanup(); +// reject(event); +// } + +// worker.postMessage({ +// data, +// difficulty, +// nonce: i, +// threads, +// jsCode, +// }); +// } +// }); +// } + export default function process( options: ProcessOptions, data: string, @@ -20,9 +117,19 @@ export default function process( progressCallback?: ProgressCallback, threads: number = Math.trunc(Math.max(getHardwareConcurrency() / 2, 1)), ): Promise { - console.debug(options); const { basePrefix, version, algorithm } = options; + // if (!isWASMSupported) { + // return runPureJS( + // options, + // data, + // difficulty, + // signal, + // progressCallback, + // threads, + // ) + // } + return new Promise(async (resolve, reject) => { let wasmFeatures = "baseline"; diff --git a/web/js/worker/wasm2js.ts b/web/js/worker/wasm2js.ts new file mode 100644 index 00000000..15f84ace --- /dev/null +++ b/web/js/worker/wasm2js.ts @@ -0,0 +1,107 @@ +import { instantiate } from '@haribala/wasm2js'; + +export interface Args { + data: string; + difficulty: number; + nonce: number; + threads: number; + jsCode: string; +} + +interface AnubisExports { + anubis_work: (difficulty: number, initialNonce: number, threads: number) => number; + data_ptr: () => number; + result_hash_ptr: () => number; + result_hash_size: () => number; + set_data_length: (len: number) => void; + memory: any; +} + +addEventListener("message", async (event: MessageEvent) => { + const { data, difficulty, threads, jsCode } = event.data; + let { nonce } = event.data; + + const importObject = { + anubis: { + anubis_update_nonce: (nonce: number) => postMessage(nonce), + } + }; + + if (nonce !== 0) { + importObject.anubis.anubis_update_nonce = (_) => { }; + } + + const instance = instantiate(jsCode, importObject); + + const { + anubis_work, + data_ptr, + result_hash_ptr, + result_hash_size, + set_data_length, + memory + } = instance.exports as unknown as AnubisExports; + function uint8ArrayToHex(arr: Uint8Array) { + return Array.from(arr) + .map((c) => c.toString(16).padStart(2, "0")) + .join(""); + } + + function hexToUint8Array(hexString: string): Uint8Array { + // 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: Uint8Array) { + 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; + } + + writeToBuffer(hexToUint8Array(data)); + + nonce = anubis_work(difficulty, nonce, threads); + const challenge = readFromChallenge(); + const result = uint8ArrayToHex(challenge); + + postMessage({ + hash: result, + difficulty, + nonce, + }); +}); \ No newline at end of file