@btx-tools/matmul-wasm
v0.1.2
Published
WASM matmul solver kernel for @btx-tools/challenges-sdk — byte-exact port of btxd's matmul service-challenge PoW
Maintainers
Readme
@btx-tools/matmul-wasm
A WASM matmul solver for BTX service challenges — a byte-exact Rust→WASM port
of btxd's matmul service-challenge proof-of-work. It produces a (nonce, digest)
proof that btxd will accept on redemption, without a local node.
Read the perf truth first. This is the fastest JS-environment solver for BTX challenges (~24× a pure-JS BigInt implementation) — genuinely useful on server / Node / edge / Deno / Bun / Workers. It is not a drop-in casual browser captcha: at the live block-mining difficulty (
n=512) a full solve is ~16 s on an 8-worker browser pool, because the difficulty is calibrated to the chain's fast native solver and a browser is ~100× slower per attempt. See Performance for the numbers and When to use it for where that 16 s is and isn't acceptable.
Part of the @btx-tools BTX admission toolkit,
alongside @btx-tools/challenges-sdk.
What it is
btxd's matmul PoW: per nonce, derive a sigma from the block header, generate a
low-rank noise perturbation of two seed matrices A, B, run a canonical blocked
matrix multiply over the Mersenne-31 field (q = 2³¹−1), hash the transcript,
and accept if digest ≤ target. This crate ports that hot loop to Rust and
compiles it to WebAssembly via wasm-bindgen.
Correctness is the whole point. The output is validated byte-exact three
independent ways: against btxd's own pinned golden vectors, against the pure-JS
reference (@btx-tools/challenges-sdk's solveJs), and in a real browser worker.
A wrong proof is worthless, so the port is pinned to byte-equality, not "close
enough."
Performance
Per-attempt cost at the live production params (n=512, b=16, r=8), scalar WASM
(no SIMD):
| engine | per-attempt | source |
|---|---|---|
| V8 (Node, Apple silicon) | 128 ms | pkg-node micro-bench |
| SpiderMonkey (Firefox) | 165 ms | in-browser bench |
Cost scales ~n³ (measured, Node): n=512 → 128 ms, n=256 → 15 ms (fixed
per-attempt overhead makes small-n deviate from a clean cubic).
8-worker browser pool (hardwareConcurrency=8) ≈ 48 attempts/s, so at floor
difficulty (~770 attempts) a full solve is ~16 s. Artifact size: ~51 KB
(wasm-opt -Oz --enable-bulk-memory).
Why not just turn on SIMD? A 2–4× SIMD win doesn't bridge the ~100×
browser-vs-native gap at n=512 (~4–8 s at floor — still not a casual captcha).
And n is policy-fixed by the chain (issued = 512; the issue RPC controls only
difficulty/time, not n), so an issuer cannot request a browser-friendly smaller
matrix. A full-strength, fast, no-node browser captcha needs an upstream
browser-friendly proof primitive — that's a BTX-protocol question, not something
this crate can close. The full measurement write-up is the public evidence for
that ask.
When to use it
✅ A fast server / Node / edge solver — solve on a caller's behalf in a two-tier deploy, or anywhere running without a local btxd. ~24× the pure-JS path.
✅ A deliberate high-friction gate — account creation / KYC-alternative, where a one-time ~16 s solve is intended friction (not a per-request captcha).
✅ The foundation for any future smaller-n or upstream browser-friendly
primitive — the kernel is done and validated.
❌ A casual per-request browser captcha at n=512 — 16 s ≠ a captcha. Don't.
API
import init, { WasmSolver } from '@btx-tools/matmul-wasm'; // web target
// const { WasmSolver } = require('@btx-tools/matmul-wasm'); // nodejs target
await init(); // web only; nodejs target loads synchronously
const solver = new WasmSolver(
version, prevhash, merkleroot, time, bits, // header fields (hex/uint)
n, b, r, // matmul params
seedA, seedB, target, // BE-display hex (64 chars)
);
// Try `maxTries` nonces from `start`, stepping by `stride`. Returns
// undefined if none found in this chunk. For an N-worker pool, worker k runs
// solve_chunk(BigInt(k), BigInt(N), chunk) and the first to return wins.
const sol = solver.solve_chunk(0n, 1n, 1_000_000n);
if (sol) {
sol.nonce_hex; // 16-char hex
sol.digest_hex; // BE-display hex (what btxd's redeem expects)
}WasmSolver validates the challenge once in its constructor (throws a catchable
JsError on malformed input); solve_chunk is then panic-free, so a worker's
loop never produces an opaque WASM trap.
Build from source
wasm-pack build --target web --release --out-dir pkg # browser ESM
wasm-pack build --target nodejs --release --out-dir pkg-node # Node CommonJS
# or, to produce a scoped npm artifact for both targets:
./scripts/build-npm.shRequires the Rust toolchain + wasm-pack. wasm-opt -Oz --enable-bulk-memory
runs automatically (the --enable-bulk-memory flag is mandatory — rustc emits
memory.copy/memory.fill).
cargo test # 24 byte-exact + edge tests
cargo clippy --all-targets -- -D warningsLicense
Dual-licensed under either MIT or Apache-2.0 at your option.
