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 <me@xeiaso.net>
This commit is contained in:
Xe Iaso
2025-09-30 14:21:29 +00:00
parent 705da2fa3c
commit 643b4719d8
13 changed files with 205 additions and 26 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}
}
}

View File

@@ -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
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

View File

@@ -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"

View File

@@ -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
};

View File

@@ -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<string> {
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,
});
}
});

View File

@@ -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
};

106
web/js/worker/wasm2js.ts Normal file
View File

@@ -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<string, AnubisExports> = {
"hashx": hashx as AnubisExports,
"sha256": sha256 as AnubisExports,
};
addEventListener("message", async (event: MessageEvent<Args>) => {
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,
});
});