diff --git a/docs/guide/widget.md b/docs/guide/widget.md index 5733056b..6188c148 100644 --- a/docs/guide/widget.md +++ b/docs/guide/widget.md @@ -4,7 +4,7 @@ outline: [2, 3, 4] # Widget -Cap's client-side widget handles requesting, solving and displaying challenges using a native web component and rust-flavoured WASM. It also includes the [programmatic mode](./programmatic). +Cap's client-side widget handles requesting, solving and displaying challenges using a native web component and a C-based WASM solver. It also includes the [programmatic mode](./programmatic). ## Installation diff --git a/docs/guide/workings.md b/docs/guide/workings.md index cfa6cff5..ebf8df4a 100644 --- a/docs/guide/workings.md +++ b/docs/guide/workings.md @@ -15,7 +15,7 @@ By the way, this is a more technical explanation of how Cap works. If you're loo #### Computing the solution -5. The widget uses Rust-flavoured WASM and Web Workers to solve the challenges in parallel: +5. The widget uses a C-based WASM solver and Web Workers to solve the challenges in parallel: - Each worker attempts to find a valid nonce by repeatedly: - Combining the salt with different nonce values - Computing the SHA-256 hash of this combination diff --git a/wasm/benchmark/README.md b/wasm/benchmark/README.md new file mode 100644 index 00000000..737b652f --- /dev/null +++ b/wasm/benchmark/README.md @@ -0,0 +1,42 @@ +# Benchmark + +This benchmark compares the historical Rust solver from commit `da725dba93f61099d264bc597d22ff388d09d2ad` against the current C solver at `HEAD`. + +## What it does + +- Rebuilds the Rust implementation from `benchmark/rust` +- Rebuilds the current C implementation from `src/c` +- Loads both Node targets +- Verifies the same challenge cases before timing them +- Reports average time per solve and the relative speedup + +## Run it + +```bash +bun --cwd benchmark run bench +``` + +Optional environment variables: + +- `BENCH_ITERATIONS` controls the timed loops, default `10` +- `BENCH_WARMUP` controls warmup rounds, default `2` + +## Browser version + +This repo also includes a browser-hosted version that loads the wasm modules in a real browser +context. + +```bash +bun --cwd benchmark run bench:browser +``` + +Then open the printed local URL. You can override the browser benchmark loop counts with query +parameters like `?iterations=20&warmup=3`. + +## Requirements + +- `make` +- `clang` +- `git` +- `rustup` +- the `wasm32-unknown-unknown` target installed for the rustup-managed toolchain diff --git a/wasm/benchmark/bench.js b/wasm/benchmark/bench.js new file mode 100644 index 00000000..65e3c259 --- /dev/null +++ b/wasm/benchmark/bench.js @@ -0,0 +1,62 @@ +import path from "node:path"; +import { createRequire } from "node:module"; +import { buildAll, headDir, rustDir } from "./build-artifacts.js"; +import { benchmarkSolver, rustBaselineCommit, verifySolver } from "./shared.js"; + +const iterations = Number.parseInt(process.env.BENCH_ITERATIONS ?? "10", 10); +const warmupRounds = Number.parseInt(process.env.BENCH_WARMUP ?? "2", 10); + +if (!Number.isFinite(iterations) || iterations < 1) { + throw new Error("BENCH_ITERATIONS must be a positive integer"); +} + +if (!Number.isFinite(warmupRounds) || warmupRounds < 0) { + throw new Error("BENCH_WARMUP must be a non-negative integer"); +} + +function loadSolvePow(modulePath) { + const require = createRequire(import.meta.url); + const mod = require(modulePath); + + if (typeof mod.solve_pow !== "function") { + throw new Error(`missing solve_pow in ${modulePath}`); + } + + return mod.solve_pow; +} + +buildAll(); + +const headSolvePow = loadSolvePow(path.join(headDir, "cap_wasm.js")); +const rustSolvePow = loadSolvePow(path.join(rustDir, "cap_wasm.js")); + +verifySolver("HEAD (C)", headSolvePow); +verifySolver(`Rust (${rustBaselineCommit.slice(0, 7)})`, rustSolvePow); + +const headResult = benchmarkSolver("HEAD (C)", headSolvePow, { + iterations, + warmupRounds, +}); +const rustResult = benchmarkSolver(`Rust (${rustBaselineCommit.slice(0, 7)})`, rustSolvePow, { + iterations, + warmupRounds, +}); + +if (headResult.checksum !== rustResult.checksum) { + throw new Error( + `checksum mismatch: ${headResult.checksum.toString()} != ${rustResult.checksum.toString()}`, + ); +} + +console.log(`\nBenchmark results`); +console.log( + `${headResult.label.padEnd(14)} ${headResult.elapsedMs.toFixed(2)} ms total ` + + `(${headResult.perSolveMs.toFixed(4)} ms/solve)`, +); +console.log( + `${rustResult.label.padEnd(14)} ${rustResult.elapsedMs.toFixed(2)} ms total ` + + `(${rustResult.perSolveMs.toFixed(4)} ms/solve)`, +); +console.log( + `Speedup: ${(rustResult.perSolveMs / headResult.perSolveMs).toFixed(2)}x faster on HEAD`, +); diff --git a/wasm/benchmark/browser/bench.js b/wasm/benchmark/browser/bench.js new file mode 100644 index 00000000..22c58dfa --- /dev/null +++ b/wasm/benchmark/browser/bench.js @@ -0,0 +1,103 @@ +import { benchmarkSolver, challengeCases, rustBaselineCommit, verifySolver } from "../shared.js"; + +const statusEl = document.getElementById("status"); +const metaEl = document.getElementById("meta"); +const outputEl = document.getElementById("output"); + +function log(line) { + outputEl.textContent += `${line}\n`; + console.log(line); +} + +function fail(error) { + const message = error instanceof Error ? error.stack ?? error.message : String(error); + statusEl.textContent = "Failed."; + outputEl.textContent += `${message}\n`; + console.error(error); +} + +window.addEventListener("error", (event) => { + fail(event.error ?? event.message); +}); + +window.addEventListener("unhandledrejection", (event) => { + fail(event.reason); +}); + +function readIntParam(name, fallback) { + const value = new URLSearchParams(location.search).get(name); + + if (value === null) { + return fallback; + } + + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed < 0) { + throw new Error(`${name} must be a non-negative integer`); + } + + return parsed; +} + +const iterations = readIntParam("iterations", 10); +const warmupRounds = readIntParam("warmup", 2); + +metaEl.textContent = `iterations=${iterations}, warmup=${warmupRounds}, cases=${challengeCases.length}`; + +try { + const cacheToken = Date.now(); + const headModule = await import(`/out/head/browser/cap_wasm.js?cache=${cacheToken}`); + const rustModule = await import(`/out/rust/browser/cap_wasm.js?cache=${cacheToken}`); + + const initHead = headModule.default; + const headSolvePow = headModule.solve_pow; + const initRust = rustModule.default; + const rustSolvePow = rustModule.solve_pow; + + statusEl.textContent = "Loading wasm modules..."; + log("Loading wasm modules..."); + await Promise.all([initHead(), initRust()]); + log("Wasm modules loaded."); + + statusEl.textContent = "Verifying solver outputs..."; + log("Verifying solver outputs..."); + await new Promise((resolve) => requestAnimationFrame(resolve)); + + verifySolver("HEAD (C)", headSolvePow); + verifySolver(`Rust (${rustBaselineCommit.slice(0, 7)})`, rustSolvePow); + log("Verification complete."); + + statusEl.textContent = "Running benchmark..."; + log("Running benchmark..."); + await new Promise((resolve) => requestAnimationFrame(resolve)); + + const headResult = benchmarkSolver("HEAD (C)", headSolvePow, { + iterations, + warmupRounds, + }); + const rustResult = benchmarkSolver(`Rust (${rustBaselineCommit.slice(0, 7)})`, rustSolvePow, { + iterations, + warmupRounds, + }); + + if (headResult.checksum !== rustResult.checksum) { + throw new Error( + `checksum mismatch: ${headResult.checksum.toString()} != ${rustResult.checksum.toString()}`, + ); + } + + log("Benchmark results"); + log( + `${headResult.label.padEnd(14)} ${headResult.elapsedMs.toFixed(2)} ms total ` + + `(${headResult.perSolveMs.toFixed(4)} ms/solve)`, + ); + log( + `${rustResult.label.padEnd(14)} ${rustResult.elapsedMs.toFixed(2)} ms total ` + + `(${rustResult.perSolveMs.toFixed(4)} ms/solve)`, + ); + log(`Speedup: ${(rustResult.perSolveMs / headResult.perSolveMs).toFixed(2)}x faster on HEAD`); + + statusEl.textContent = "Done."; +} catch (error) { + fail(error); +} diff --git a/wasm/benchmark/browser/index.html b/wasm/benchmark/browser/index.html new file mode 100644 index 00000000..dba2e4ca --- /dev/null +++ b/wasm/benchmark/browser/index.html @@ -0,0 +1,109 @@ + + + + + + Wasm Browser Benchmark + + + + +
+

Wasm Browser Benchmark

+

+ Runs the C and Rust solvers in an actual browser context and compares their average solve + time. +

+
Waiting to load wasm modules...
+
+

+    
+ + + diff --git a/wasm/benchmark/build-artifacts.js b/wasm/benchmark/build-artifacts.js new file mode 100644 index 00000000..bdfbd299 --- /dev/null +++ b/wasm/benchmark/build-artifacts.js @@ -0,0 +1,144 @@ +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +export const __dirname = path.dirname(fileURLToPath(import.meta.url)); +export const rootDir = path.join(__dirname, ".."); +export const rustupBin = path.join(process.env.HOME ?? "", ".cargo", "bin", "rustup"); +export const rustcBin = path.join(process.env.HOME ?? "", ".cargo", "bin", "rustc"); +export const rustBaselineCommit = "da725dba93f61099d264bc597d22ff388d09d2ad"; +export const outDir = path.join(__dirname, "out"); +export const headDir = path.join(outDir, "head"); +export const headBuildDir = path.join(headDir, "build"); +export const headBrowserDir = path.join(headDir, "browser"); +export const nodeLoaderSrc = path.join(rootDir, "src", "node", "cap_wasm.js"); +export const browserLoaderSrc = path.join(rootDir, "src", "browser", "cap_wasm.js"); +export const headLoaderDest = path.join(headDir, "cap_wasm.js"); +export const headWasm = path.join(headDir, "cap_wasm_bg.wasm"); +export const rustDir = path.join(outDir, "rust"); +export const rustBrowserDir = path.join(rustDir, "browser"); +export const rustSrcDir = path.join(__dirname, "rust"); + +function resetDir(dir) { + fs.rmSync(dir, { recursive: true, force: true }); + fs.mkdirSync(dir, { recursive: true }); +} + +export function buildHead() { + resetDir(headDir); + fs.mkdirSync(headBrowserDir, { recursive: true }); + + execFileSync( + "make", + [ + "-C", + path.join(rootDir, "src", "c"), + `BUILD_DIR=${headBuildDir}`, + `OUT=${path.join(headBuildDir, "cap_wasm.wasm")}`, + `NODE_WASM=${headWasm}`, + `BROWSER_WASM=${path.join(headBrowserDir, "cap_wasm_bg.wasm")}`, + ], + { + cwd: rootDir, + stdio: "inherit", + }, + ); + + fs.copyFileSync(nodeLoaderSrc, headLoaderDest); + fs.copyFileSync(browserLoaderSrc, path.join(headBrowserDir, "cap_wasm.js")); +} + +export function buildRust() { + resetDir(rustDir); + fs.mkdirSync(rustBrowserDir, { recursive: true }); + + execFileSync( + rustupBin, + [ + "run", + "stable", + "cargo", + "build", + "--manifest-path", + path.join(rustSrcDir, "Cargo.toml"), + "--target", + "wasm32-unknown-unknown", + "--release", + ], + { + cwd: rootDir, + env: { + ...process.env, + CARGO_TARGET_DIR: path.join(rustDir, "target"), + RUSTC: rustcBin, + }, + stdio: "inherit", + }, + ); + + const rustWasmSource = path.join( + rustDir, + "target", + "wasm32-unknown-unknown", + "release", + "cap_wasm.wasm", + ); + + const rustNodeLoader = execFileSync( + "git", + ["show", `${rustBaselineCommit}:wasm/src/node/cap_wasm.js`], + { + cwd: rootDir, + encoding: "utf8", + maxBuffer: 16 * 1024 * 1024, + }, + ); + + const rustNodeLoaderWithStubs = rustNodeLoader + .replace( + "imports = {};", + [ + "imports = {};", + "module.exports.__wbindgen_describe = function () {};", + "module.exports.__wbindgen_externref_xform__ = {", + " __wbindgen_externref_table_set_null: function () {},", + " __wbindgen_externref_table_grow: function () { return 0; },", + "};", + "imports.__wbindgen_externref_xform__ = module.exports.__wbindgen_externref_xform__;", + ].join("\n"), + ) + .replace("wasm.__wbindgen_start());", "wasm.__wbindgen_start && wasm.__wbindgen_start());"); + + fs.writeFileSync(path.join(rustDir, "cap_wasm.js"), rustNodeLoaderWithStubs); + fs.copyFileSync(rustWasmSource, path.join(rustDir, "cap_wasm_bg.wasm")); + const rustBrowserTemplate = fs.readFileSync(browserLoaderSrc, "utf8"); + const rustBrowserLoader = rustBrowserTemplate.replace( + /function __wbg_get_imports\(\) \{[\s\S]*?\n\}\nfunction __wbg_init_memory/, + `function __wbg_get_imports() { + const e = { __wbindgen_placeholder__: {}, __wbindgen_externref_xform__: {} }; + e.__wbindgen_placeholder__.__wbindgen_describe = function () {}; + e.__wbindgen_externref_xform__.__wbindgen_externref_table_set_null = function () {}; + e.__wbindgen_externref_xform__.__wbindgen_externref_table_grow = function () { return 0; }; + e.__wbindgen_placeholder__.__wbindgen_externref_xform__ = e.__wbindgen_externref_xform__; + e.__wbindgen_placeholder__.__wbindgen_init_externref_table = function () { + const e = wasm.__wbindgen_export_0, + t = e.grow(4); + e.set(0, void 0); + e.set(t + 0, void 0); + e.set(t + 1, null); + e.set(t + 2, !0); + e.set(t + 3, !1); + }; + return e; +} +function __wbg_init_memory`, + ).replace("wasm.__wbindgen_start(),", "wasm.__wbindgen_start && wasm.__wbindgen_start(),"); + fs.writeFileSync(path.join(rustBrowserDir, "cap_wasm.js"), rustBrowserLoader); + fs.copyFileSync(rustWasmSource, path.join(rustBrowserDir, "cap_wasm_bg.wasm")); +} + +export function buildAll() { + buildHead(); + buildRust(); +} diff --git a/wasm/benchmark/package.json b/wasm/benchmark/package.json new file mode 100644 index 00000000..49020cda --- /dev/null +++ b/wasm/benchmark/package.json @@ -0,0 +1,9 @@ +{ + "name": "@cap.js/wasm-benchmark", + "private": true, + "type": "module", + "scripts": { + "bench": "bun run bench.js", + "bench:browser": "bun run serve-browser.js" + } +} diff --git a/wasm/benchmark/rust/.gitignore b/wasm/benchmark/rust/.gitignore new file mode 100644 index 00000000..fede2a05 --- /dev/null +++ b/wasm/benchmark/rust/.gitignore @@ -0,0 +1,3 @@ +Cargo.lock +pkg +target diff --git a/wasm/src/rust/Cargo.toml b/wasm/benchmark/rust/Cargo.toml similarity index 100% rename from wasm/src/rust/Cargo.toml rename to wasm/benchmark/rust/Cargo.toml diff --git a/wasm/src/rust/src/lib.rs b/wasm/benchmark/rust/src/lib.rs similarity index 96% rename from wasm/src/rust/src/lib.rs rename to wasm/benchmark/rust/src/lib.rs index ed70b2a0..5f07e0ff 100644 --- a/wasm/src/rust/src/lib.rs +++ b/wasm/benchmark/rust/src/lib.rs @@ -1,29 +1,29 @@ +use sha2::{Digest, Sha256}; use wasm_bindgen::prelude::*; -use sha2::{Sha256, Digest}; #[wasm_bindgen] pub fn solve_pow(salt: String, target: String) -> u64 { let salt_bytes = salt.as_bytes(); - + let target_bytes = parse_hex_target(&target); let target_bits = target.len() * 4; // each hex char = 4 bits - + let mut nonce_buffer = [0u8; 20]; // u64::MAX has at most 20 digits - + for nonce in 0..u64::MAX { let nonce_len = write_u64_to_buffer(nonce, &mut nonce_buffer); let nonce_bytes = &nonce_buffer[..nonce_len]; - + let mut hasher = Sha256::new(); hasher.update(salt_bytes); hasher.update(nonce_bytes); let hash_result = hasher.finalize(); - + if hash_matches_target(&hash_result, &target_bytes, target_bits) { return nonce; } } - + unreachable!("Solution should be found before exhausting u64::MAX"); } @@ -45,37 +45,37 @@ fn write_u64_to_buffer(mut value: u64, buffer: &mut [u8]) -> usize { buffer[0] = b'0'; return 1; } - + let mut len = 0; let mut temp = value; - + while temp > 0 { len += 1; temp /= 10; } - + for i in (0..len).rev() { buffer[i] = (value % 10) as u8 + b'0'; value /= 10; } - + len } fn hash_matches_target(hash: &[u8], target_bytes: &[u8], target_bits: usize) -> bool { let full_bytes = target_bits / 8; let remaining_bits = target_bits % 8; - + if hash[..full_bytes] != target_bytes[..full_bytes] { return false; } - + if remaining_bits > 0 && full_bytes < target_bytes.len() { let mask = 0xFF << (8 - remaining_bits); let hash_masked = hash[full_bytes] & mask; let target_masked = target_bytes[full_bytes] & mask; return hash_masked == target_masked; } - + true -} \ No newline at end of file +} diff --git a/wasm/benchmark/serve-browser.js b/wasm/benchmark/serve-browser.js new file mode 100644 index 00000000..59d134f3 --- /dev/null +++ b/wasm/benchmark/serve-browser.js @@ -0,0 +1,95 @@ +import fs from "node:fs"; +import path from "node:path"; +import { buildAll, __dirname } from "./build-artifacts.js"; + +const rootDir = __dirname; +const requestedPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : null; +const host = process.env.HOST ?? "127.0.0.1"; +const mimeTypes = new Map([ + [".html", "text/html; charset=utf-8"], + [".js", "text/javascript; charset=utf-8"], + [".mjs", "text/javascript; charset=utf-8"], + [".css", "text/css; charset=utf-8"], + [".wasm", "application/wasm"], + [".json", "application/json; charset=utf-8"], +]); + +function resolveFile(requestUrl) { + const url = new URL(requestUrl, "http://localhost"); + const pathname = url.pathname === "/" ? "/browser/index.html" : url.pathname; + const filePath = path.join(rootDir, pathname); + const normalizedRoot = path.resolve(rootDir); + const normalizedFile = path.resolve(filePath); + + if (!normalizedFile.startsWith(normalizedRoot + path.sep) && normalizedFile !== normalizedRoot) { + return null; + } + + return normalizedFile; +} + +await buildAll(); + +function createServer(port) { + return Bun.serve({ + hostname: host, + port, + fetch(req) { + const filePath = resolveFile(new URL(req.url).pathname); + + if (!filePath) { + return new Response("Forbidden", { status: 403 }); + } + + let stat; + try { + stat = fs.statSync(filePath); + } catch { + return new Response("Not found", { status: 404 }); + } + + const targetPath = stat.isDirectory() ? path.join(filePath, "index.html") : filePath; + let body; + try { + body = fs.readFileSync(targetPath); + } catch { + return new Response("Not found", { status: 404 }); + } + + const ext = path.extname(targetPath); + return new Response(body, { + headers: { + "Content-Type": mimeTypes.get(ext) ?? "application/octet-stream", + "Cache-Control": "no-cache", + }, + }); + }, + }); +} + +const candidatePorts = + requestedPort !== null && Number.isFinite(requestedPort) + ? [requestedPort] + : Array.from({ length: 200 }, (_, index) => 49152 + ((Date.now() + index) % 1000)).concat([ + 8787, + 4173, + 8081, + 18080, + 3000, + ]); + +let lastError; +for (const candidatePort of candidatePorts) { + try { + const server = createServer(candidatePort); + console.log(`Serving ${path.join(rootDir, "browser", "index.html")} at ${server.url}`); + lastError = null; + break; + } catch (error) { + lastError = error; + } +} + +if (lastError) { + throw lastError; +} diff --git a/wasm/benchmark/shared.js b/wasm/benchmark/shared.js new file mode 100644 index 00000000..6b402a02 --- /dev/null +++ b/wasm/benchmark/shared.js @@ -0,0 +1,69 @@ +export const rustBaselineCommit = "da725dba93f61099d264bc597d22ff388d09d2ad"; + +export const challengeCases = [ + ["e455cea65e98bc3c36287f43769da211", "dceb", 63625], + ["fb8d25f6abac5aa9b6360051f37e010b", "93f1", 62420], + ["91ef47db578fbeb2565d3f9c82bb7960", "3698", 42515], + ["b7ad7667486a691cda8ef297098f64a7", "d72a", 32395], + ["1aca3fb7cef7a2be0dee563ed4136758", "3b58", 53368], + ["d9ec39af92b430e5a329274d8aa58fa8", "e1d3", 52431], + ["781a3cc9217d73c908a321d3fdabd62f", "22c6", 102156], + ["e37a0752c9ac2f3d2517747fde373ac9", "f6f1", 118925], + ["bba070197569f322beda5b240f639a95", "4751", 25523], + ["89297515aeac646bee9653ba405e0beb", "a7de", 47980], + ["444571a0d5039c15be6141d6cd8434f9", "a783", 38735], + ["ba75f2bf8e9b92cc32caa17237a52d14", "7e30", 67790], + ["22bfc18ba8e3ecee080c5d1ef64ed6e9", "5fcf", 14836], + ["885fb78ff76b4eddd2f5bc04ac5ee673", "93e5", 91445], + ["308758072931bb3b254a7b1ed351d04a", "3e49", 19889], + ["724f89bb167db4b881e1dc7b0949ac8f", "b82e", 28170], + ["8b79506e4630de15be225c18623eff65", "f0e5", 9820], + ["0c21ade6e63a4e37b13cb8b087f31863", "65c9", 190704], +]; + +export function verifySolver(label, solvePow) { + for (const [salt, target, expectedNonce] of challengeCases) { + const actualNonce = solvePow(salt, target).toString(); + + if (actualNonce !== String(expectedNonce)) { + throw new Error( + `${label} mismatch for ${salt}:${target} - expected ${expectedNonce}, got ${actualNonce}`, + ); + } + } +} + +export function benchmarkSolver(label, solvePow, { iterations, warmupRounds }) { + if (!Number.isFinite(iterations) || iterations < 1) { + throw new Error("iterations must be a positive integer"); + } + + if (!Number.isFinite(warmupRounds) || warmupRounds < 0) { + throw new Error("warmupRounds must be a non-negative integer"); + } + + for (let round = 0; round < warmupRounds; round += 1) { + for (const [salt, target] of challengeCases) { + solvePow(salt, target); + } + } + + const start = performance.now(); + let checksum = 0n; + + for (let round = 0; round < iterations; round += 1) { + for (const [salt, target] of challengeCases) { + checksum ^= solvePow(salt, target); + } + } + + const elapsedMs = performance.now() - start; + const totalCalls = iterations * challengeCases.length; + + return { + label, + checksum, + elapsedMs, + perSolveMs: elapsedMs / totalCalls, + }; +} diff --git a/wasm/build.js b/wasm/build.js index bc977bd4..b4abc177 100644 --- a/wasm/build.js +++ b/wasm/build.js @@ -3,38 +3,18 @@ import fs from "node:fs"; import path from "node:path"; import { minify } from "terser"; -const rustSrcDir = path.join(process.cwd(), "./src/rust"); +const cSrcDir = path.join(process.cwd(), "./src/c"); const nodeOutDir = path.join(process.cwd(), "./src/node"); const browserOutDir = path.join(process.cwd(), "./src/browser"); -const packageName = "cap_wasm"; +const wasmOutPath = path.join(cSrcDir, "build", "cap_wasm.wasm"); -console.log(`Cleaning old build directories...`); -try { - fs.rmdirSync(nodeOutDir); -} catch {} -try { - fs.rmdirSync(browserOutDir); -} catch {} +console.log(`Building C wasm...`); +execSync(`make -C "${cSrcDir}" clean`, { stdio: "inherit" }); +execSync(`make -C "${cSrcDir}"`, { stdio: "inherit" }); -console.log(`\n Building for Node...`); -execSync( - `wasm-pack build "${rustSrcDir}" --target nodejs --out-dir "${nodeOutDir}" --out-name "${packageName}"`, - { stdio: "inherit" }, -); - -console.log(`\n Building for web...`); -execSync( - `wasm-pack build "${rustSrcDir}" --target web --out-dir "${browserOutDir}" --out-name "${packageName}"`, - { stdio: "inherit" }, -); - -console.log(`\n Removing .gitignore...`); - -[browserOutDir, nodeOutDir].forEach((dir) => { - try { - fs.rmSync(path.join(dir, ".gitignore")); - } catch {} -}); +console.log(`\n Syncing wasm binary to package outputs...`); +fs.copyFileSync(wasmOutPath, path.join(nodeOutDir, "cap_wasm_bg.wasm")); +fs.copyFileSync(wasmOutPath, path.join(browserOutDir, "cap_wasm_bg.wasm")); console.log(`\n Minifing loaders...`); @@ -56,10 +36,14 @@ if (!doTest) { } console.log(`\n test...`); -execSync(`bun ${path.join("test", "node.js")}`, { stdio: "inherit" }); +execSync(`bun test ${path.join("c", "test-regression.test.js")}`, { + stdio: "inherit", + cwd: "./src", +}); +execSync(`bun test ${path.join("test", "node.test.js")}`, { stdio: "inherit" }); console.log(`\n testing odd difficulty...`); -execSync(`bun ${path.join("test", "node_odd_difficulty.js")}`, { +execSync(`bun test ${path.join("test", "node_odd_difficulty.test.js")}`, { stdio: "inherit", }); diff --git a/wasm/src/README.md b/wasm/src/README.md index 06463799..8d049394 100644 --- a/wasm/src/README.md +++ b/wasm/src/README.md @@ -1 +1,3 @@ # Cap WASM + +C implementation of Cap's proof-of-work solver, compiled to WebAssembly for the browser and Node.js. diff --git a/wasm/src/browser/cap_wasm_bg.wasm b/wasm/src/browser/cap_wasm_bg.wasm index e2134b54..d3b96da6 100644 Binary files a/wasm/src/browser/cap_wasm_bg.wasm and b/wasm/src/browser/cap_wasm_bg.wasm differ diff --git a/wasm/src/c/Makefile b/wasm/src/c/Makefile new file mode 100644 index 00000000..597b48a5 --- /dev/null +++ b/wasm/src/c/Makefile @@ -0,0 +1,31 @@ +CC = clang +TARGET ?= wasm32-unknown-unknown +BUILD_DIR ?= build +OUT ?= $(BUILD_DIR)/cap_wasm.wasm +NODE_WASM := ../node/cap_wasm_bg.wasm +BROWSER_WASM := ../browser/cap_wasm_bg.wasm + +CFLAGS ?= -O3 -ffreestanding -fno-builtin -fvisibility=hidden --target=$(TARGET) +INCLUDES := -Iinclude +LDFLAGS ?= -nostdlib -Wl,--no-entry -Wl,--export=solve_pow -Wl,--export=__wbindgen_malloc -Wl,--export=__wbindgen_realloc -Wl,--export=__wbindgen_start -Wl,--export-memory + +SRC := src/cap_wasm.c + +.PHONY: all clean sync-node sync-browser + +all: $(OUT) sync-node sync-browser + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +$(OUT): $(SRC) include/cap_wasm.h | $(BUILD_DIR) + $(CC) $(CFLAGS) $(INCLUDES) $(SRC) $(LDFLAGS) -o $(OUT) + +sync-node: $(OUT) + cp $(OUT) $(NODE_WASM) + +sync-browser: $(OUT) + cp $(OUT) $(BROWSER_WASM) + +clean: + rm -rf $(BUILD_DIR) diff --git a/wasm/src/c/include/cap_wasm.h b/wasm/src/c/include/cap_wasm.h new file mode 100644 index 00000000..babd0a89 --- /dev/null +++ b/wasm/src/c/include/cap_wasm.h @@ -0,0 +1,20 @@ +#ifndef CAP_WASM_H +#define CAP_WASM_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +uint64_t solve_pow(const char *salt, uint32_t salt_len, const char *target, uint32_t target_len); + +void *__wbindgen_malloc(uint32_t size, uint32_t align); +void *__wbindgen_realloc(void *ptr, uint32_t old_size, uint32_t new_size, uint32_t align); +void __wbindgen_start(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/wasm/src/c/src/cap_wasm.c b/wasm/src/c/src/cap_wasm.c new file mode 100644 index 00000000..e43536a2 --- /dev/null +++ b/wasm/src/c/src/cap_wasm.c @@ -0,0 +1,339 @@ +#include "cap_wasm.h" + +#include + +extern unsigned char __heap_base; + +#define SHA256_BLOCK_SIZE 64u +#define SHA256_DIGEST_SIZE 32u +#define WASM_PAGE_SIZE 65536u + +typedef struct { + uint32_t state[8]; + uint64_t bitlen; + uint8_t buffer[SHA256_BLOCK_SIZE]; + uint32_t buffer_len; +} sha256_ctx; + +static uint32_t heap_cursor = 0; + +static void trap(void) { + __builtin_trap(); +} + +static uint32_t align_up(uint32_t value, uint32_t align) { + if (align <= 1u) { + return value; + } + return (value + (align - 1u)) & ~(align - 1u); +} + +static void copy_bytes(uint8_t *dst, const uint8_t *src, uint32_t len) { + for (uint32_t i = 0; i < len; ++i) { + dst[i] = src[i]; + } +} + +static void ensure_capacity(uint32_t end) { + uint32_t current_pages = (uint32_t)__builtin_wasm_memory_size(0); + uint32_t current_bytes = current_pages * WASM_PAGE_SIZE; + + if (end <= current_bytes) { + return; + } + + uint32_t needed_bytes = end - current_bytes; + uint32_t grow_pages = (needed_bytes + WASM_PAGE_SIZE - 1u) / WASM_PAGE_SIZE; + int32_t previous_pages = __builtin_wasm_memory_grow(0, (int32_t)grow_pages); + if (previous_pages < 0) { + trap(); + } +} + +static void *wasm_alloc(uint32_t size, uint32_t align) { + if (size == 0u) { + return (void *)0; + } + + if (heap_cursor == 0u) { + heap_cursor = (uint32_t)(uintptr_t)&__heap_base; + } + + uint32_t cursor = align_up(heap_cursor, align); + uint32_t end = cursor + size; + if (end < cursor) { + trap(); + } + + ensure_capacity(end); + heap_cursor = end; + return (void *)(uintptr_t)cursor; +} + +static uint32_t hex_value(char c) { + if (c >= '0' && c <= '9') { + return (uint32_t)(c - '0'); + } + if (c >= 'a' && c <= 'f') { + return (uint32_t)(c - 'a' + 10); + } + if (c >= 'A' && c <= 'F') { + return (uint32_t)(c - 'A' + 10); + } + + trap(); + return 0; +} + +static void parse_hex_target(const char *target, uint32_t target_len, uint8_t *out) { + uint32_t padded_len = target_len + (target_len & 1u); + uint32_t out_len = padded_len / 2u; + + for (uint32_t i = 0; i < out_len; ++i) { + uint32_t src_index = i * 2u; + char hi = target[src_index]; + char lo = (src_index + 1u < target_len) ? target[src_index + 1u] : '0'; + out[i] = (uint8_t)((hex_value(hi) << 4u) | hex_value(lo)); + } +} + +static uint32_t write_u64_to_buffer(uint64_t value, uint8_t *buffer) { + if (value == 0u) { + buffer[0] = (uint8_t)'0'; + return 1u; + } + + uint32_t len = 0u; + uint64_t temp = value; + + while (temp > 0u) { + ++len; + temp /= 10u; + } + + for (uint32_t i = len; i-- > 0u;) { + buffer[i] = (uint8_t)('0' + (value % 10u)); + value /= 10u; + } + + return len; +} + +static int hash_matches_target(const uint8_t *hash, const uint8_t *target_bytes, uint32_t target_bits, uint32_t target_bytes_len) { + uint32_t full_bytes = target_bits / 8u; + uint32_t remaining_bits = target_bits % 8u; + + for (uint32_t i = 0; i < full_bytes; ++i) { + if (hash[i] != target_bytes[i]) { + return 0; + } + } + + if (remaining_bits > 0u && full_bytes < target_bytes_len) { + uint8_t mask = (uint8_t)(0xFFu << (8u - remaining_bits)); + uint8_t hash_masked = (uint8_t)(hash[full_bytes] & mask); + uint8_t target_masked = (uint8_t)(target_bytes[full_bytes] & mask); + return hash_masked == target_masked; + } + + return 1; +} + +static uint32_t rotr32(uint32_t value, uint32_t bits) { + return (value >> bits) | (value << (32u - bits)); +} + +static uint32_t load_be32(const uint8_t *p) { + return ((uint32_t)p[0] << 24u) | ((uint32_t)p[1] << 16u) | ((uint32_t)p[2] << 8u) | (uint32_t)p[3]; +} + +static void store_be32(uint8_t *p, uint32_t value) { + p[0] = (uint8_t)(value >> 24u); + p[1] = (uint8_t)(value >> 16u); + p[2] = (uint8_t)(value >> 8u); + p[3] = (uint8_t)value; +} + +static void store_be64(uint8_t *p, uint64_t value) { + p[0] = (uint8_t)(value >> 56u); + p[1] = (uint8_t)(value >> 48u); + p[2] = (uint8_t)(value >> 40u); + p[3] = (uint8_t)(value >> 32u); + p[4] = (uint8_t)(value >> 24u); + p[5] = (uint8_t)(value >> 16u); + p[6] = (uint8_t)(value >> 8u); + p[7] = (uint8_t)value; +} + +static void sha256_transform(sha256_ctx *ctx, const uint8_t block[SHA256_BLOCK_SIZE]) { + static const uint32_t k[64] = { + 0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u, + 0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u, + 0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau, + 0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u, + 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u, + 0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u, + 0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u, + 0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u + }; + + uint32_t w[64]; + + for (uint32_t i = 0; i < 16u; ++i) { + w[i] = load_be32(block + (i * 4u)); + } + + for (uint32_t i = 16u; i < 64u; ++i) { + uint32_t s0 = rotr32(w[i - 15u], 7u) ^ rotr32(w[i - 15u], 18u) ^ (w[i - 15u] >> 3u); + uint32_t s1 = rotr32(w[i - 2u], 17u) ^ rotr32(w[i - 2u], 19u) ^ (w[i - 2u] >> 10u); + w[i] = w[i - 16u] + s0 + w[i - 7u] + s1; + } + + uint32_t a = ctx->state[0]; + uint32_t b = ctx->state[1]; + uint32_t c = ctx->state[2]; + uint32_t d = ctx->state[3]; + uint32_t e = ctx->state[4]; + uint32_t f = ctx->state[5]; + uint32_t g = ctx->state[6]; + uint32_t h = ctx->state[7]; + + for (uint32_t i = 0; i < 64u; ++i) { + uint32_t s1 = rotr32(e, 6u) ^ rotr32(e, 11u) ^ rotr32(e, 25u); + uint32_t ch = (e & f) ^ ((~e) & g); + uint32_t temp1 = h + s1 + ch + k[i] + w[i]; + uint32_t s0 = rotr32(a, 2u) ^ rotr32(a, 13u) ^ rotr32(a, 22u); + uint32_t maj = (a & b) ^ (a & c) ^ (b & c); + uint32_t temp2 = s0 + maj; + + h = g; + g = f; + f = e; + e = d + temp1; + d = c; + c = b; + b = a; + a = temp1 + temp2; + } + + ctx->state[0] += a; + ctx->state[1] += b; + ctx->state[2] += c; + ctx->state[3] += d; + ctx->state[4] += e; + ctx->state[5] += f; + ctx->state[6] += g; + ctx->state[7] += h; +} + +static void sha256_init(sha256_ctx *ctx) { + ctx->state[0] = 0x6a09e667u; + ctx->state[1] = 0xbb67ae85u; + ctx->state[2] = 0x3c6ef372u; + ctx->state[3] = 0xa54ff53au; + ctx->state[4] = 0x510e527fu; + ctx->state[5] = 0x9b05688cu; + ctx->state[6] = 0x1f83d9abu; + ctx->state[7] = 0x5be0cd19u; + ctx->bitlen = 0u; + ctx->buffer_len = 0u; +} + +static void sha256_update(sha256_ctx *ctx, const uint8_t *data, uint32_t len) { + ctx->bitlen += (uint64_t)len * 8u; + + while (len > 0u) { + uint32_t space = SHA256_BLOCK_SIZE - ctx->buffer_len; + uint32_t to_copy = len < space ? len : space; + + copy_bytes(ctx->buffer + ctx->buffer_len, data, to_copy); + ctx->buffer_len += to_copy; + data += to_copy; + len -= to_copy; + + if (ctx->buffer_len == SHA256_BLOCK_SIZE) { + sha256_transform(ctx, ctx->buffer); + ctx->buffer_len = 0u; + } + } +} + +static void sha256_final(sha256_ctx *ctx, uint8_t out[SHA256_DIGEST_SIZE]) { + uint32_t i = ctx->buffer_len; + + ctx->buffer[i++] = 0x80u; + + if (i > 56u) { + while (i < SHA256_BLOCK_SIZE) { + ctx->buffer[i++] = 0u; + } + sha256_transform(ctx, ctx->buffer); + i = 0u; + } + + while (i < 56u) { + ctx->buffer[i++] = 0u; + } + + store_be64(ctx->buffer + 56u, ctx->bitlen); + sha256_transform(ctx, ctx->buffer); + + for (uint32_t j = 0; j < 8u; ++j) { + store_be32(out + (j * 4u), ctx->state[j]); + } +} + +uint64_t solve_pow(const char *salt, uint32_t salt_len, const char *target, uint32_t target_len) { + uint8_t nonce_buffer[20]; + uint32_t target_bits = target_len * 4u; + uint32_t target_bytes_len = (target_len + (target_len & 1u)) / 2u; + uint8_t *target_bytes = 0; + + if (target_bytes_len > 0u) { + target_bytes = (uint8_t *)__builtin_alloca(target_bytes_len); + parse_hex_target(target, target_len, target_bytes); + } + + for (uint64_t nonce = 0u; nonce != UINT64_MAX; ++nonce) { + uint32_t nonce_len = write_u64_to_buffer(nonce, nonce_buffer); + uint8_t hash[SHA256_DIGEST_SIZE]; + + sha256_ctx ctx; + sha256_init(&ctx); + sha256_update(&ctx, (const uint8_t *)salt, salt_len); + sha256_update(&ctx, nonce_buffer, nonce_len); + sha256_final(&ctx, hash); + + if (hash_matches_target(hash, target_bytes, target_bits, target_bytes_len)) { + return nonce; + } + } + + trap(); + return 0u; +} + +void *__wbindgen_malloc(uint32_t size, uint32_t align) { + return wasm_alloc(size, align); +} + +void *__wbindgen_realloc(void *ptr, uint32_t old_size, uint32_t new_size, uint32_t align) { + if (new_size == 0u) { + return (void *)0; + } + + void *new_ptr = wasm_alloc(new_size, align); + if (new_ptr == 0) { + trap(); + } + + uint32_t copy_size = old_size < new_size ? old_size : new_size; + if (ptr != 0 && copy_size > 0u) { + copy_bytes((uint8_t *)new_ptr, (const uint8_t *)ptr, copy_size); + } + + return new_ptr; +} + +void __wbindgen_start(void) { +} diff --git a/wasm/src/c/test-regression.test.js b/wasm/src/c/test-regression.test.js new file mode 100644 index 00000000..cbf6add2 --- /dev/null +++ b/wasm/src/c/test-regression.test.js @@ -0,0 +1,74 @@ +import fs from "node:fs"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { execFileSync } from "node:child_process"; +import { test, expect } from "bun:test"; + +const require = createRequire(import.meta.url); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +function build() { + execFileSync("make", ["-C", "c"], { stdio: "inherit" }); +} + +function loadSolvePow(modulePath) { + const mod = require(modulePath); + if (typeof mod.solve_pow !== "function") { + throw new Error(`missing solve_pow in ${modulePath}`); + } + return mod.solve_pow; +} + +async function loadBrowserSolvePow() { + const browserModule = await import( + pathToFileURL(path.join(__dirname, "..", "browser", "cap_wasm.js")).href + ); + browserModule.initSync({ + module: fs.readFileSync(path.join(__dirname, "..", "browser", "cap_wasm_bg.wasm")), + }); + return browserModule.solve_pow; +} + +function assertCases(solvePow, cases) { + for (const [salt, target, expectedNonce] of cases) { + const actualNonce = solvePow(salt, target).toString(); + expect(actualNonce).toBe(String(expectedNonce)); + } +} + +build(); + +const nodeSolvePow = loadSolvePow(path.join("..", "node", "cap_wasm.js")); + +const regressionCases = [ + ["e455cea65e98bc3c36287f43769da211", "dceb", 63625], + ["fb8d25f6abac5aa9b6360051f37e010b", "93f1", 62420], + ["91ef47db578fbeb2565d3f9c82bb7960", "3698", 42515], + ["b7ad7667486a691cda8ef297098f64a7", "d72a", 32395], + ["1aca3fb7cef7a2be0dee563ed4136758", "3b58", 53368], + ["d9ec39af92b430e5a329274d8aa58fa8", "e1d3", 52431], + ["781a3cc9217d73c908a321d3fdabd62f", "22c6", 102156], + ["e37a0752c9ac2f3d2517747fde373ac9", "f6f1", 118925], + ["bba070197569f322beda5b240f639a95", "4751", 25523], + ["89297515aeac646bee9653ba405e0beb", "a7de", 47980], + ["444571a0d5039c15be6141d6cd8434f9", "a783", 38735], + ["ba75f2bf8e9b92cc32caa17237a52d14", "7e30", 67790], + ["22bfc18ba8e3ecee080c5d1ef64ed6e9", "5fcf", 14836], + ["885fb78ff76b4eddd2f5bc04ac5ee673", "93e5", 91445], + ["308758072931bb3b254a7b1ed351d04a", "3e49", 19889], + ["724f89bb167db4b881e1dc7b0949ac8f", "b82e", 28170], + ["8b79506e4630de15be225c18623eff65", "f0e5", 9820], + ["0c21ade6e63a4e37b13cb8b087f31863", "65c9", 190704], +]; + +const browserCases = [regressionCases[0], regressionCases[7], regressionCases[17]]; + +test("node wrapper regression", () => { + assertCases(nodeSolvePow, regressionCases); +}); + +test("browser wrapper regression", async () => { + const browserSolvePow = await loadBrowserSolvePow(); + assertCases(browserSolvePow, browserCases); +}); diff --git a/wasm/src/node/cap_wasm_bg.wasm b/wasm/src/node/cap_wasm_bg.wasm index cb33c138..d3b96da6 100644 Binary files a/wasm/src/node/cap_wasm_bg.wasm and b/wasm/src/node/cap_wasm_bg.wasm differ diff --git a/wasm/src/package.json b/wasm/src/package.json index 7c55c7cb..406eed99 100644 --- a/wasm/src/package.json +++ b/wasm/src/package.json @@ -1,7 +1,7 @@ { "name": "@cap.js/wasm", "version": "0.0.6", - "description": "WASM solver for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.", + "description": "C-based WASM solver for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.", "keywords": [ "algorithm", "anti-abuse", @@ -52,6 +52,7 @@ }, "main": "./node/cap_wasm.js", "scripts": { + "test": "bun test ./c/test-regression.test.js", "npm:publish": "bun publish --access public" } } diff --git a/wasm/src/rust/Cargo.lock b/wasm/src/rust/Cargo.lock deleted file mode 100644 index 6d1978a1..00000000 --- a/wasm/src/rust/Cargo.lock +++ /dev/null @@ -1,218 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "cap_wasm" -version = "0.1.0" -dependencies = [ - "hex", - "sha2", - "wasm-bindgen", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "libc" -version = "0.2.172" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "syn" -version = "2.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] diff --git a/wasm/test/node.js b/wasm/test/node.js deleted file mode 100644 index fd4e21af..00000000 --- a/wasm/test/node.js +++ /dev/null @@ -1,41 +0,0 @@ -import { performance } from "node:perf_hooks"; -import { solve_pow } from "../src/node/cap_wasm.js"; - -const challenges = [ - ["e455cea65e98bc3c36287f43769da211", "dceb"], - ["fb8d25f6abac5aa9b6360051f37e010b", "93f1"], - ["91ef47db578fbeb2565d3f9c82bb7960", "3698"], - ["b7ad7667486a691cda8ef297098f64a7", "d72a"], - ["1aca3fb7cef7a2be0dee563ed4136758", "3b58"], - ["d9ec39af92b430e5a329274d8aa58fa8", "e1d3"], - ["781a3cc9217d73c908a321d3fdabd62f", "22c6"], - ["e37a0752c9ac2f3d2517747fde373ac9", "f6f1"], - ["bba070197569f322beda5b240f639a95", "4751"], - ["89297515aeac646bee9653ba405e0beb", "a7de"], - ["444571a0d5039c15be6141d6cd8434f9", "a783"], - ["ba75f2bf8e9b92cc32caa17237a52d14", "7e30"], - ["22bfc18ba8e3ecee080c5d1ef64ed6e9", "5fcf"], - ["885fb78ff76b4eddd2f5bc04ac5ee673", "93e5"], - ["308758072931bb3b254a7b1ed351d04a", "3e49"], - ["724f89bb167db4b881e1dc7b0949ac8f", "b82e"], - ["8b79506e4630de15be225c18623eff65", "f0e5"], - ["0c21ade6e63a4e37b13cb8b087f31863", "65c9"], -]; - -async function runSolverTest() { - const startTime = performance.now(); - - for (let i = 0; i < challenges.length; i++) { - const [salt, target] = challenges[i]; - const nonce = solve_pow(salt, target); - - console.log(`[${i + 1}/${challenges.length}] ${salt}:${target}:${nonce}`); - } - - const endTime = performance.now(); - const totalTime = (endTime - startTime) / 1000; - - console.log(`Solved challenges in ${totalTime.toFixed(3)}s`); -} - -runSolverTest(); diff --git a/wasm/test/node.test.js b/wasm/test/node.test.js new file mode 100644 index 00000000..55d3bc02 --- /dev/null +++ b/wasm/test/node.test.js @@ -0,0 +1,41 @@ +import { performance } from "node:perf_hooks"; +import { test, expect } from "bun:test"; +import { solve_pow } from "../src/node/cap_wasm.js"; + +const challenges = [ + ["e455cea65e98bc3c36287f43769da211", "dceb", 63625], + ["fb8d25f6abac5aa9b6360051f37e010b", "93f1", 62420], + ["91ef47db578fbeb2565d3f9c82bb7960", "3698", 42515], + ["b7ad7667486a691cda8ef297098f64a7", "d72a", 32395], + ["1aca3fb7cef7a2be0dee563ed4136758", "3b58", 53368], + ["d9ec39af92b430e5a329274d8aa58fa8", "e1d3", 52431], + ["781a3cc9217d73c908a321d3fdabd62f", "22c6", 102156], + ["e37a0752c9ac2f3d2517747fde373ac9", "f6f1", 118925], + ["bba070197569f322beda5b240f639a95", "4751", 25523], + ["89297515aeac646bee9653ba405e0beb", "a7de", 47980], + ["444571a0d5039c15be6141d6cd8434f9", "a783", 38735], + ["ba75f2bf8e9b92cc32caa17237a52d14", "7e30", 67790], + ["22bfc18ba8e3ecee080c5d1ef64ed6e9", "5fcf", 14836], + ["885fb78ff76b4eddd2f5bc04ac5ee673", "93e5", 91445], + ["308758072931bb3b254a7b1ed351d04a", "3e49", 19889], + ["724f89bb167db4b881e1dc7b0949ac8f", "b82e", 28170], + ["8b79506e4630de15be225c18623eff65", "f0e5", 9820], + ["0c21ade6e63a4e37b13cb8b087f31863", "65c9", 190704], +]; + +test("node solver regression cases", () => { + const startTime = performance.now(); + + for (let i = 0; i < challenges.length; i++) { + const [salt, target, expectedNonce] = challenges[i]; + const nonce = solve_pow(salt, target); + expect(nonce.toString()).toBe(String(expectedNonce)); + + console.log(`[${i + 1}/${challenges.length}] ${salt}:${target}:${nonce}`); + } + + const endTime = performance.now(); + const totalTime = (endTime - startTime) / 1000; + + console.log(`Solved challenges in ${totalTime.toFixed(3)}s`); +}); diff --git a/wasm/test/node_odd_difficulty.js b/wasm/test/node_odd_difficulty.test.js similarity index 53% rename from wasm/test/node_odd_difficulty.js rename to wasm/test/node_odd_difficulty.test.js index a6a369fd..0f506f40 100644 --- a/wasm/test/node_odd_difficulty.js +++ b/wasm/test/node_odd_difficulty.test.js @@ -1,15 +1,19 @@ import { createHash } from "node:crypto"; +import { test, expect } from "bun:test"; import { solve_pow } from "../src/node/cap_wasm.js"; // salt, target const challenge = ["02679e6558", "eeffc"]; -const [salt, target] = challenge; -const nonce = solve_pow(salt, target); +test("odd difficulty challenge", () => { + const [salt, target] = challenge; + const nonce = solve_pow(salt, target); + expect(nonce.toString()).toBe("1127415"); -const actualHash = createHash("sha256").update(`${salt}${nonce}`).digest("hex"); + const actualHash = createHash("sha256").update(`${salt}${nonce}`).digest("hex"); + expect(actualHash.slice(0, target.length)).toBe(target); -console.log(`salt: ${salt} + console.log(`salt: ${salt} target: ${target} nonce: ${nonce} @@ -19,3 +23,4 @@ should start with ${target} is ${actualHash.slice(0, target.length)} (${actualHash}) ${actualHash.slice(0, target.length) === target ? "✅ success!" : "❌ invalid"}`); +});