From 643b4719d836ca0e43ab65c7f65a6b094f40c73b Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Tue, 30 Sep 2025 14:21:29 +0000 Subject: [PATCH] feat(wasm): support "pure JS" mode Closes #1159 This uses the binaryen tool wasm2js to compile the Anubis WASM blobs to JavaScript. This produces biblically large (520Ki) outputs when you inline both hashx and sha256 solvers, but this is a tradeoff that I'm willing to accept. The performance is good enough in my testing with JIT enabled. I fear that this may end up being terrible with JIT disabled. I have no idea if this will work on big endian or not. Signed-off-by: Xe Iaso --- .devcontainer/Dockerfile | 2 +- .github/workflows/docker-pr.yml | 3 + .github/workflows/docker.yml | 3 + .github/workflows/go.yml | 3 + .github/workflows/package-builds-stable.yml | 3 + .github/workflows/package-builds-unstable.yml | 3 + package.json | 7 +- scripts/build_wasm.sh | 11 +- web/build.sh | 2 +- web/js/algorithms/wasm/hashx.ts | 23 ++++ web/js/algorithms/{wasm.ts => wasm/index.ts} | 42 +++---- web/js/algorithms/wasm/sha256.ts | 23 ++++ web/js/worker/wasm2js.ts | 106 ++++++++++++++++++ 13 files changed, 205 insertions(+), 26 deletions(-) create mode 100644 web/js/algorithms/wasm/hashx.ts rename web/js/algorithms/{wasm.ts => wasm/index.ts} (62%) create mode 100644 web/js/algorithms/wasm/sha256.ts create mode 100644 web/js/worker/wasm2js.ts diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 8021a174..d4164e6d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app COPY go.mod go.sum package.json package-lock.json ./ RUN apt-get update \ - && apt-get -y install zstd brotli redis uuid-runtime \ + && apt-get -y install zstd brotli redis uuid-runtime binaryen \ && mkdir -p /home/vscode/.local/share/fish \ && chown -R vscode:vscode /home/vscode/.local/share/fish \ && chown -R vscode:vscode /go diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml index 41b3f98a..7a9f199b 100644 --- a/.github/workflows/docker-pr.yml +++ b/.github/workflows/docker-pr.yml @@ -50,6 +50,9 @@ jobs: cache: false target: wasm32-unknown-unknown + - name: Setup Binaryen + uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0 + - name: Docker meta id: meta uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bd360862..dbf91e07 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -60,6 +60,9 @@ jobs: cache: false target: wasm32-unknown-unknown + - name: Setup Binaryen + uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0 + - name: Log into registry uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f88fe41f..46790474 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -63,6 +63,9 @@ jobs: cache: false target: wasm32-unknown-unknown + - name: Setup Binaryen + uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0 + - name: Cache playwright binaries uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 id: playwright-cache diff --git a/.github/workflows/package-builds-stable.yml b/.github/workflows/package-builds-stable.yml index 063899fd..84c7b7f9 100644 --- a/.github/workflows/package-builds-stable.yml +++ b/.github/workflows/package-builds-stable.yml @@ -64,6 +64,9 @@ jobs: cache: false target: wasm32-unknown-unknown + - name: Setup Binaryen + uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0 + - name: install node deps run: | npm ci diff --git a/.github/workflows/package-builds-unstable.yml b/.github/workflows/package-builds-unstable.yml index a1e81092..cf8e4597 100644 --- a/.github/workflows/package-builds-unstable.yml +++ b/.github/workflows/package-builds-unstable.yml @@ -65,6 +65,9 @@ jobs: cache: false target: wasm32-unknown-unknown + - name: Setup Binaryen + uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0 + - name: install node deps run: | npm ci diff --git a/package.json b/package.json index ff20e4ef..639f7526 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,11 @@ "test:integration": "npm run assets && go test -v ./internal/test", "test:integration:podman": "npm run assets && go test -v ./internal/test --playwright-runner=podman", "test:integration:docker": "npm run assets && go test -v ./internal/test --playwright-runner=docker", + "generate": "go generate ./...", + "assets:js": "./web/build.sh", + "assets:css": "./xess/build.sh", "assets:wasm": "bash ./scripts/build_wasm.sh", - "assets": "go generate ./... && ./web/build.sh && ./xess/build.sh && npm run assets:wasm", + "assets": "npm run generate && npm run assets:wasm && npm run assets:js && npm run assets:css", "build": "npm run assets && go build -o ./var/anubis ./cmd/anubis", "dev": "npm run assets && go run ./cmd/anubis --use-remote-address --target http://localhost:3000", "container": "npm run assets && go run ./cmd/containerbuild", @@ -37,4 +40,4 @@ "preact": "^10.27.2", "wasm-feature-detect": "^1.8.0" } -} +} \ No newline at end of file diff --git a/scripts/build_wasm.sh b/scripts/build_wasm.sh index dcd93d9d..59d14280 100755 --- a/scripts/build_wasm.sh +++ b/scripts/build_wasm.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -euo pipefail + mkdir -p ./web/static/wasm/{simd128,baseline} cargo clean @@ -12,4 +14,11 @@ cargo clean # Without simd128 cargo build --release --target wasm32-unknown-unknown -cp -vf ./target/wasm32-unknown-unknown/release/*.wasm ./web/static/wasm/baseline \ No newline at end of file +cp -vf ./target/wasm32-unknown-unknown/release/*.wasm ./web/static/wasm/baseline + +for file in ./web/static/wasm/baseline/*.wasm; do + echo $file + rm -f ${file%.*}.wasmjs + wasm2js $file -all -O4 --strip-debug --rse --rereloop --optimize-for-js --flatten --dce --dfo --fpcast-emu --denan --dealign --remove-imports --remove-unused-names --remove-unused-brs --reorder-functions --reorder-locals --strip-target-features --untee --vacuum -s 4 -ffm -lmu -tnh -iit -n -o ${file%.*}.mjs + sed -i '1s$.*$const anubis_update_nonce = (_ignored) => { };$' ${file%.*}.mjs +done \ No newline at end of file diff --git a/web/build.sh b/web/build.sh index cebd3c86..364fdca9 100755 --- a/web/build.sh +++ b/web/build.sh @@ -49,7 +49,7 @@ for file in js/**/*.ts js/**/*.mjs; do mkdir -p "$(dirname "$out")" - esbuild "$file" --sourcemap --minify --bundle --outfile="$out" --banner:js="$LICENSE" + esbuild "$file" --sourcemap --bundle --outfile="$out" --banner:js="$LICENSE" gzip -f -k -n "$out" zstd -f -k --ultra -22 "$out" brotli -fZk "$out" diff --git a/web/js/algorithms/wasm/hashx.ts b/web/js/algorithms/wasm/hashx.ts new file mode 100644 index 00000000..b66e9e51 --- /dev/null +++ b/web/js/algorithms/wasm/hashx.ts @@ -0,0 +1,23 @@ +import { + memory, + data_ptr, + set_data_length, + anubis_work, + anubis_validate, + result_hash_ptr, + result_hash_size, + verification_hash_ptr, + verification_hash_size, +} from "../../../static/wasm/baseline/hashx.mjs"; + +export default { + memory, + data_ptr, + set_data_length, + anubis_work, + anubis_validate, + result_hash_ptr, + result_hash_size, + verification_hash_ptr, + verification_hash_size +}; \ No newline at end of file diff --git a/web/js/algorithms/wasm.ts b/web/js/algorithms/wasm/index.ts similarity index 62% rename from web/js/algorithms/wasm.ts rename to web/js/algorithms/wasm/index.ts index 1a39dc9c..d949fed0 100644 --- a/web/js/algorithms/wasm.ts +++ b/web/js/algorithms/wasm/index.ts @@ -1,4 +1,4 @@ -import { u } from "../../lib/xeact"; +import { u } from "../../../lib/xeact"; import { simd } from "wasm-feature-detect"; // import { compile } from '@haribala/wasm2js'; @@ -13,20 +13,21 @@ 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; -})(); +// // 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 isWASMSupported = false; export default function process( options: ProcessOptions, @@ -38,8 +39,10 @@ export default function process( ): Promise { const { basePrefix, version, algorithm } = options; + let worker = "wasm"; + if (!isWASMSupported) { - throw new Error("WebAssembly is not supported on this platform. Please report ALL details about your browser, environment, OS, CPU, device vendor, and other details to https://github.com/TecharoHQ/anubis/issues/1116. Remember: being polite means you get this fixed."); + worker = "wasm2js"; } return new Promise(async (resolve, reject) => { @@ -49,10 +52,7 @@ export default function process( wasmFeatures = "simd128"; } - const module = await fetch(u(`${basePrefix}/.within.website/x/cmd/anubis/static/wasm/${wasmFeatures}/${algorithm}.wasm?cacheBuster=${version}`)) - .then(x => WebAssembly.compileStreaming(x)); - - const webWorkerURL = `${options.basePrefix}/.within.website/x/cmd/anubis/static/js/worker/wasm.mjs?cacheBuster=${version}`; + const webWorkerURL = `${basePrefix}/.within.website/x/cmd/anubis/static/js/worker/${worker}.mjs?cacheBuster=${version}`; const workers: Worker[] = []; let settled = false; @@ -103,7 +103,7 @@ export default function process( difficulty, nonce: i, threads, - module, + algorithm, }); } }); diff --git a/web/js/algorithms/wasm/sha256.ts b/web/js/algorithms/wasm/sha256.ts new file mode 100644 index 00000000..7940b492 --- /dev/null +++ b/web/js/algorithms/wasm/sha256.ts @@ -0,0 +1,23 @@ +import { + memory, + data_ptr, + set_data_length, + anubis_work, + anubis_validate, + result_hash_ptr, + result_hash_size, + verification_hash_ptr, + verification_hash_size, +} from "../../../static/wasm/baseline/sha256.mjs"; + +export default { + memory, + data_ptr, + set_data_length, + anubis_work, + anubis_validate, + result_hash_ptr, + result_hash_size, + verification_hash_ptr, + verification_hash_size +}; \ No newline at end of file diff --git a/web/js/worker/wasm2js.ts b/web/js/worker/wasm2js.ts new file mode 100644 index 00000000..c32cffcb --- /dev/null +++ b/web/js/worker/wasm2js.ts @@ -0,0 +1,106 @@ +import hashx from "../algorithms/wasm/hashx"; +import sha256 from "../algorithms/wasm/sha256"; + +export interface Args { + data: string; + difficulty: number; + nonce: number; + threads: number; + algorithm: 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: WebAssembly.Memory; +} + +const algorithms: Record = { + "hashx": hashx as AnubisExports, + "sha256": sha256 as AnubisExports, +}; + +addEventListener("message", async (event: MessageEvent) => { + const { data, difficulty, threads, algorithm } = event.data; + let { nonce } = event.data; + + const obj = algorithms[algorithm]; + if (obj == undefined) { + throw new Error(`unknown algorithm ${algorithm}, file a bug please`); + } + + const { + anubis_work, + data_ptr, + result_hash_ptr, + result_hash_size, + set_data_length, + memory + } = obj; + 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