diff --git a/.gitignore b/.gitignore
index a716c66f..4accc2bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,7 @@ node_modules
# how does this get here
doc/VERSION
+
+*.wasm
+
+target
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 00000000..57bdde7b
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,100 @@
+# 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 = "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 = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.171"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
+
+[[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 = "sha256"
+version = "0.1.0"
+dependencies = [
+ "lazy_static",
+ "sha2",
+]
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 00000000..9e5a7ac4
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,9 @@
+[workspace]
+resolver = "2"
+members = ["wasm/pow/*"]
+
+[profile.release]
+strip = true
+opt-level = "s"
+lto = "thin"
+codegen-units = 1
diff --git a/wasm/pow/sha256/Cargo.toml b/wasm/pow/sha256/Cargo.toml
new file mode 100644
index 00000000..c2bf025b
--- /dev/null
+++ b/wasm/pow/sha256/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "sha256"
+version = "0.1.0"
+edition = "2024"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+lazy_static = "1.5"
+sha2 = "0.10"
+
+[lints.clippy]
+nursery = { level = "warn", priority = -1 }
+pedantic = { level = "warn", priority = -1 }
+unwrap_used = "warn"
+uninlined_format_args = "allow"
+missing_panics_doc = "allow"
+missing_errors_doc = "allow"
+cognitive_complexity = "allow"
diff --git a/wasm/pow/sha256/run.html b/wasm/pow/sha256/run.html
new file mode 100644
index 00000000..2e6f2354
--- /dev/null
+++ b/wasm/pow/sha256/run.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/wasm/pow/sha256/run.js b/wasm/pow/sha256/run.js
new file mode 100644
index 00000000..8f9c5e0e
--- /dev/null
+++ b/wasm/pow/sha256/run.js
@@ -0,0 +1,105 @@
+// Load and instantiate the .wasm file
+const response = await fetch("sha256.wasm");
+
+const importObject = {
+ anubis: {
+ anubis_update_nonce: (nonce) => {
+ console.log(`Received nonce update: ${nonce}`);
+ // Your logic here
+ }
+ }
+};
+
+const module = await WebAssembly.compileStreaming(response);
+const instance = await WebAssembly.instantiate(module, importObject);
+
+// Get exports
+const {
+ anubis_work,
+ anubis_validate,
+ data_ptr,
+ result_hash_ptr,
+ result_hash_size,
+ verification_hash_ptr,
+ verification_hash_size,
+ set_data_length,
+ memory
+} = instance.exports;
+
+console.log(instance.exports);
+
+function uint8ArrayToHex(arr) {
+ return Array.from(arr)
+ .map((c) => c.toString(16).padStart(2, "0"))
+ .join("");
+}
+
+function hexToUint8Array(hexString) {
+ // 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) {
+ 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;
+}
+
+// Example usage:
+const data = hexToUint8Array("98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4");
+writeToBuffer(data);
+
+// Call work function
+const t0 = Date.now();
+const nonce = anubis_work(16, 0, 1);
+const t1 = Date.now();
+
+console.log(`Done! Took ${t1 - t0}ms, ${nonce} iterations`);
+
+const challengeBuffer = readFromChallenge();
+
+{
+ const buffer = new Uint8Array(memory.buffer, verification_hash_ptr(), verification_hash_size());
+ buffer.set(challengeBuffer);
+}
+
+// Validate
+const isValid = anubis_validate(nonce, 10) === 1;
+console.log(isValid);
+
+console.log(uint8ArrayToHex(readFromChallenge()));
\ No newline at end of file
diff --git a/wasm/pow/sha256/src/lib.rs b/wasm/pow/sha256/src/lib.rs
new file mode 100644
index 00000000..fd53bdf9
--- /dev/null
+++ b/wasm/pow/sha256/src/lib.rs
@@ -0,0 +1,139 @@
+use lazy_static::lazy_static;
+use sha2::{Digest, Sha256};
+use std::sync::Mutex;
+
+lazy_static! {
+ static ref DATA_BUFFER: Mutex<[u8; 1024]> = Mutex::new([0; 1024]);
+ static ref DATA_LENGTH: Mutex = Mutex::new(0);
+ static ref RESULT_HASH: Mutex<[u8; 32]> = Mutex::new([0; 32]);
+ static ref VERIFICATION_HASH: Mutex<[u8; 32]> = Mutex::new([0; 32]);
+}
+
+#[link(wasm_import_module = "anubis")] // Usually matches your JS namespace
+unsafe extern "C" {
+ // Declare the imported function
+ fn anubis_update_nonce(nonce: u32);
+}
+
+fn update_nonce(nonce: u32) {
+ unsafe {
+ anubis_update_nonce(nonce);
+ }
+}
+
+/// Core validation function
+fn validate(hash: &[u8], difficulty: u32) -> bool {
+ let mut remaining = difficulty;
+ for &byte in hash {
+ if remaining == 0 {
+ break;
+ }
+ if remaining >= 8 {
+ if byte != 0 {
+ return false;
+ }
+ remaining -= 8;
+ } else {
+ let mask = 0xFF << (8 - remaining);
+ if (byte & mask) != 0 {
+ return false;
+ }
+ remaining = 0;
+ }
+ }
+ true
+}
+
+/// Computes hash for given nonce
+fn compute_hash(nonce: u32) -> [u8; 32] {
+ let data = DATA_BUFFER.lock().unwrap();
+ let data_len = *DATA_LENGTH.lock().unwrap();
+ let use_le = data[data_len - 1] >= 128;
+
+ let data_slice = &data[..data_len];
+
+ let mut hasher = Sha256::new();
+ hasher.update(data_slice);
+ hasher.update(if use_le {
+ nonce.to_le_bytes()
+ } else {
+ nonce.to_be_bytes()
+ });
+ hasher.finalize().into()
+}
+
+// WebAssembly exports
+
+#[unsafe(no_mangle)]
+pub extern "C" fn anubis_work(difficulty: u32, initial_nonce: u32, iterand: u32) -> u32 {
+ let mut nonce = initial_nonce;
+
+ loop {
+ let hash = compute_hash(nonce);
+
+ if validate(&hash, difficulty) {
+ let mut challenge = RESULT_HASH.lock().unwrap();
+ challenge.copy_from_slice(&hash);
+ return nonce;
+ }
+
+ let old_nonce = nonce;
+ nonce = nonce.wrapping_add(iterand);
+
+ // send a progress update every 1024 iterations. since each thread checks
+ // separate values, one simple way to do this is by bit masking the
+ // nonce for multiples of 1024. unfortunately, if the number of threads
+ // is not prime, only some of the threads will be sending the status
+ // update and they will get behind the others. this is slightly more
+ // complicated but ensures an even distribution between threads.
+ if nonce > old_nonce | 1023 && (nonce >> 10) % iterand == initial_nonce {
+ update_nonce(nonce);
+ }
+ }
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn anubis_validate(nonce: u32, difficulty: u32) -> bool {
+ let computed = compute_hash(nonce);
+ let valid = validate(&computed, difficulty);
+
+ let verification = VERIFICATION_HASH.lock().unwrap();
+ valid && computed == *verification
+}
+
+// Memory accessors
+
+#[unsafe(no_mangle)]
+pub extern "C" fn result_hash_ptr() -> *const u8 {
+ let challenge = RESULT_HASH.lock().unwrap();
+ challenge.as_ptr()
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn result_hash_size() -> usize {
+ RESULT_HASH.lock().unwrap().len()
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn verification_hash_ptr() -> *const u8 {
+ let verification = VERIFICATION_HASH.lock().unwrap();
+ verification.as_ptr()
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn verification_hash_size() -> usize {
+ VERIFICATION_HASH.lock().unwrap().len()
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn data_ptr() -> *const u8 {
+ let challenge = DATA_BUFFER.lock().unwrap();
+ challenge.as_ptr()
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn set_data_length(len: u32) {
+ // Add missing length setter
+ let mut data_length = DATA_LENGTH.lock().unwrap();
+ *data_length = len as usize;
+}
diff --git a/web/js/proof-of-work.mjs b/web/js/algos/fast.mjs
similarity index 98%
rename from web/js/proof-of-work.mjs
rename to web/js/algos/fast.mjs
index 5ef3a8af..a077c226 100644
--- a/web/js/proof-of-work.mjs
+++ b/web/js/algos/fast.mjs
@@ -5,7 +5,6 @@ export default function process(
progressCallback = null,
threads = (navigator.hardwareConcurrency || 1),
) {
- console.debug("fast algo");
return new Promise((resolve, reject) => {
let webWorkerURL = URL.createObjectURL(new Blob([
'(', processTask(), ')()'
@@ -99,7 +98,6 @@ function processTask() {
if (valid) {
hash = uint8ArrayToHexString(thisHash);
- console.log(hash);
break;
}
diff --git a/web/js/algos/sha256.mjs b/web/js/algos/sha256.mjs
new file mode 100644
index 00000000..cfea8769
--- /dev/null
+++ b/web/js/algos/sha256.mjs
@@ -0,0 +1,160 @@
+import { u } from "../xeact.mjs";
+
+export default function process(
+ data,
+ difficulty = 16,
+ signal = null,
+ pc = null,
+ threads = (navigator.hardwareConcurrency || 1),
+) {
+ return new Promise(async (resolve, reject) => {
+ let webWorkerURL = URL.createObjectURL(new Blob([
+ '(', processTask(), ')()'
+ ], { type: 'application/javascript' }));
+
+ const module = await fetch(u("/.within.website/x/cmd/anubis/static/wasm/sha256.wasm"))
+ .then(resp => WebAssembly.compileStreaming(resp));
+
+ const workers = [];
+ const terminate = () => {
+ workers.forEach((w) => w.terminate());
+ if (signal != null) {
+ // clean up listener to avoid memory leak
+ signal.removeEventListener("abort", terminate);
+ if (signal.aborted) {
+ console.log("PoW aborted");
+ reject(false);
+ }
+ }
+ };
+ if (signal != null) {
+ signal.addEventListener("abort", terminate, { once: true });
+ }
+
+ for (let i = 0; i < threads; i++) {
+ let worker = new Worker(webWorkerURL);
+
+ worker.onmessage = (event) => {
+ if (typeof event.data === "number") {
+ pc?.(event.data);
+ } else {
+ terminate();
+ resolve(event.data);
+ }
+ };
+
+ worker.onerror = (event) => {
+ terminate();
+ reject(event);
+ };
+
+ worker.postMessage({
+ data,
+ difficulty,
+ nonce: i,
+ threads,
+ module,
+ });
+
+ workers.push(worker);
+ }
+
+ URL.revokeObjectURL(webWorkerURL);
+ });
+}
+
+function processTask() {
+ return function () {
+ addEventListener('message', async (event) => {
+ const importObject = {
+ anubis: {
+ anubis_update_nonce: (nonce) => postMessage(nonce),
+ }
+ };
+
+ const instance = await WebAssembly.instantiate(event.data.module, importObject);
+
+ // Get exports
+ const {
+ anubis_work,
+ data_ptr,
+ result_hash_ptr,
+ result_hash_size,
+ set_data_length,
+ memory
+ } = instance.exports;
+
+ function uint8ArrayToHex(arr) {
+ return Array.from(arr)
+ .map((c) => c.toString(16).padStart(2, "0"))
+ .join("");
+ }
+
+ function hexToUint8Array(hexString) {
+ // 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) {
+ 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;
+ }
+
+ let data = event.data.data;
+ let difficulty = event.data.difficulty;
+ let hash;
+ let nonce = event.data.nonce;
+ let interand = event.data.threads;
+
+ writeToBuffer(hexToUint8Array(data));
+
+ nonce = anubis_work(difficulty, nonce, interand);
+ const challenge = readFromChallenge();
+
+ data = uint8ArrayToHex(challenge);
+
+ postMessage({
+ hash: data,
+ difficulty,
+ nonce,
+ });
+ });
+ }.toString();
+}
+
diff --git a/web/js/proof-of-work-slow.mjs b/web/js/algos/slow.mjs
similarity index 98%
rename from web/js/proof-of-work-slow.mjs
rename to web/js/algos/slow.mjs
index 0bdc1466..23048599 100644
--- a/web/js/proof-of-work-slow.mjs
+++ b/web/js/algos/slow.mjs
@@ -7,7 +7,6 @@ export default function process(
progressCallback = null,
_threads = 1,
) {
- console.debug("slow algo");
return new Promise((resolve, reject) => {
let webWorkerURL = URL.createObjectURL(new Blob([
'(', processTask(), ')()'
diff --git a/web/js/bench.mjs b/web/js/bench.mjs
index c8c69bdc..93daa908 100644
--- a/web/js/bench.mjs
+++ b/web/js/bench.mjs
@@ -1,10 +1,12 @@
-import processFast from "./proof-of-work.mjs";
-import processSlow from "./proof-of-work-slow.mjs";
+import fast from "./algos/fast.mjs";
+import slow from "./algos/slow.mjs";
+import sha256 from "./algos/sha256.mjs";
-const defaultDifficulty = 4;
+const defaultDifficulty = 16;
const algorithms = {
- fast: processFast,
- slow: processSlow,
+ sha256: sha256,
+ fast: fast,
+ slow: slow,
};
const status = document.getElementById("status");
@@ -41,10 +43,13 @@ const benchmarkTrial = async (stats, difficulty, algorithm, signal) => {
.map((c) => c.toString(16).padStart(2, "0"))
.join("");
+ if (algorithm != "sha256") {
+ difficulty = Math.round(difficulty / 4);
+ }
+
const t0 = performance.now();
const { hash, nonce } = await process(challenge, Number(difficulty), signal);
const t1 = performance.now();
- console.log({ hash, nonce });
stats.time += t1 - t0;
stats.iters += nonce;
diff --git a/web/js/main.mjs b/web/js/main.mjs
index a093c741..79d62e98 100644
--- a/web/js/main.mjs
+++ b/web/js/main.mjs
@@ -1,17 +1,13 @@
-import processFast from "./proof-of-work.mjs";
-import processSlow from "./proof-of-work-slow.mjs";
+import fast from "./algos/fast.mjs";
+import slow from "./algos/slow.mjs";
+import sha256 from "./algos/sha256.mjs";
import { testVideo } from "./video.mjs";
+import { u } from "./xeact.mjs";
const algorithms = {
- "fast": processFast,
- "slow": processSlow,
-};
-
-// from Xeact
-const u = (url = "", params = {}) => {
- let result = new URL(url, window.location.href);
- Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v));
- return result.toString();
+ "fast": fast,
+ "slow": slow,
+ "sha256": sha256,
};
const imageURL = (mood, cacheBuster) =>
@@ -28,6 +24,11 @@ const dependencies = [
msg: "Your browser doesn't support web workers (Anubis uses this to avoid freezing your browser). Do you have a plugin like JShelter installed?",
value: window.Worker,
},
+ {
+ name: "WebAssembly",
+ msg: "Your browser doesn't have WebAssembly support. If you are running a big endian system, I'm sorry but this is something we can't work around with a polyfill.",
+ value: window.WebAssembly,
+ },
];
function showContinueBar(hash, nonce, t0, t1) {
diff --git a/web/js/xeact.mjs b/web/js/xeact.mjs
new file mode 100644
index 00000000..5d5e8e31
--- /dev/null
+++ b/web/js/xeact.mjs
@@ -0,0 +1,13 @@
+/**
+ * Generate a relative URL from `url`, appending all key-value pairs from `params` as URL-encoded parameters.
+ *
+ * @type{function(string=, Object=): string}
+ */
+export const u = (url = "", params = {}) => {
+ let result = new URL(url, window.location.href);
+ Object.entries(params).forEach((kv) => {
+ let [k, v] = kv;
+ result.searchParams.set(k, v);
+ });
+ return result.toString();
+};
\ No newline at end of file
diff --git a/web/static/wasm/.gitignore b/web/static/wasm/.gitignore
new file mode 100644
index 00000000..c96a04f0
--- /dev/null
+++ b/web/static/wasm/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file