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