From 0dccf2e0099577e6b1793bac2aa125fd745df748 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Sat, 2 Aug 2025 11:27:26 -0400 Subject: [PATCH] refactor(web): redo proof of work web worker logic (#941) * chore(web/js): delete proof-of-work-slow.mjs This code has served its purpose and now needs to be retired to the great beyond. There is no replacement for this, the fast implementation will be used instead. Signed-off-by: Xe Iaso * chore(web): handle building multiple JS entrypoints and web workers Signed-off-by: Xe Iaso * feat(web): rewrite frontend worker handling This completely rewrites how the proof of work challenge works based on feedback from browser engine developers and starts the process of making the proof of work function easier to change out. - Import @aws-crypto/sha256-js to use in Firefox as its implementation of WebCrypto doesn't jump directly from highly optimized browser internals to JIT-ed JavaScript like Chrome's seems to. - Move the worker code to `web/js/worker/*` with each worker named after the hashing method and hash method implementation it uses. - Update bench.mjs to import algorithms the new way. - Delete video.mjs, it was part of a legacy experiment that I never had time to finish. - Update LibreJS comment to add info about the use of @aws-crypto/sha256-js. - Also update my email to my @techaro.lol address. Signed-off-by: Xe Iaso * fix(web): don't hard dep webcrypto anymore Signed-off-by: Xe Iaso * chore(lib/policy): start the deprecation process for slow This mostly adds a warning, but the "slow" method is in the process of being removed. Warn admins with slog.Warn. Signed-off-by: Xe Iaso * docs: update CHANGELOG Signed-off-by: Xe Iaso * feat(web/js): allow running Anubis in non-secure contexts Signed-off-by: Xe Iaso * Update metadata check-spelling run (pull_request) for Xe/purge-slow Signed-off-by: check-spelling-bot on-behalf-of: @check-spelling --------- Signed-off-by: Xe Iaso Signed-off-by: check-spelling-bot --- .github/actions/spelling/expect.txt | 3 +- docs/docs/CHANGELOG.md | 11 +++ lib/policy/policy.go | 8 ++ package-lock.json | 97 ++++++++++++++++++++ package.json | 5 +- web/build.sh | 17 ++-- web/js/algorithms/fast.mjs | 77 ++++++++++++++++ web/js/algorithms/index.mjs | 6 ++ web/js/bench.mjs | 9 +- web/js/main.mjs | 23 +---- web/js/proof-of-work-slow.mjs | 99 -------------------- web/js/proof-of-work.mjs | 137 ---------------------------- web/js/video.mjs | 16 ---- web/js/worker/sha256-purejs.mjs | 61 +++++++++++++ web/js/worker/sha256-webcrypto.mjs | 57 ++++++++++++ 15 files changed, 337 insertions(+), 289 deletions(-) create mode 100644 web/js/algorithms/fast.mjs create mode 100644 web/js/algorithms/index.mjs delete mode 100644 web/js/proof-of-work-slow.mjs delete mode 100644 web/js/proof-of-work.mjs delete mode 100644 web/js/video.mjs create mode 100644 web/js/worker/sha256-purejs.mjs create mode 100644 web/js/worker/sha256-webcrypto.mjs diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index d89fff5..e43c222 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -141,6 +141,7 @@ httpdebug Huawei hypertext iaskspider +iaso iat ifm Imagesift @@ -232,6 +233,7 @@ promauto promhttp proofofwork publicsuffix +purejs pwcmd pwuser qualys @@ -309,7 +311,6 @@ Varis Velen vendored vhosts -videotest VKE Vultr waitloop diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index 19a59f3..d3eba2f 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -19,6 +19,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The [`segments`](./admin/configuration/expressions.mdx#segments) function was added for splitting a path into its slash-separated segments. - When issuing a challenge, Anubis stores information about that challenge into the store. That stored information is later used to validate challenge responses. This works around nondeterminism in bot rules. ([#917](https://github.com/TecharoHQ/anubis/issues/917)) - When parsing [Open Graph tags](./admin/configuration/open-graph.mdx), add any URLs found in the responses to a temporary "allow cache" so that social preview images work. +- Proof of work solving has had a complete overhaul and rethink based on feedback from browser engine developers, frontend experts, and overall performance profiling. +- One of the biggest sources of lag in Firefox has been eliminated: the use of WebCrypto. Now whenever Anubis detects the client is using Firefox (or Pale Moon), it will swap over to a pure-JS implementation of SHA-256 for speed. +- Web Workers are stored as dedicated JavaScript files in `static/js/workers/*.mjs`. +- Pave the way for non-SHA256 solver methods and eventually one that uses WebAssembly (or WebAssembly code compiled to JS for those that disable WebAssembly). +- Legacy JavaScript code has been eliminated. +- The contact email in the LibreJS header has been changed. +- The hard dependency on WebCrypto has been removed, allowing a proof of work challenge to work over plain (unencrypted) HTTP. + +### Breaking changes + +- The "slow" frontend solver has been removed in order to reduce maintenance burden. Any existing uses of it will still work, but issue a warning upon startup asking administrators to upgrade to the "fast" frontend solver. ## v1.21.3: Minfilia Warde - Echo 3 diff --git a/lib/policy/policy.go b/lib/policy/policy.go index 3dc3157..5493d8d 100644 --- a/lib/policy/policy.go +++ b/lib/policy/policy.go @@ -149,6 +149,10 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic if parsedBot.Challenge.Algorithm == "" { parsedBot.Challenge.Algorithm = config.DefaultAlgorithm } + + if parsedBot.Challenge.Algorithm == "slow" { + slog.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", parsedBot.Name) + } } if b.Weight != nil { @@ -163,6 +167,10 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic } for _, t := range c.Thresholds { + if t.Challenge != nil && t.Challenge.Algorithm == "slow" { + slog.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", t.Name) + } + if t.Name == "legacy-anubis-behaviour" && t.Expression.String() == "true" { if !warnedAboutThresholds.Load() { slog.Warn("configuration file does not contain thresholds, see docs for details on how to upgrade", "fname", fname, "docs_url", "https://anubis.techaro.lol/docs/admin/configuration/thresholds/") diff --git a/package-lock.json b/package-lock.json index 35a0a4d..64a9ad4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@techaro/anubis", "version": "1.21.3", "license": "ISC", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0" + }, "devDependencies": { "cssnano": "^7.1.0", "cssnano-preset-advanced": "^7.0.8", @@ -19,6 +22,44 @@ "postcss-url": "^10.1.3" } }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.840.0.tgz", + "integrity": "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -461,6 +502,56 @@ "node": ">=18" } }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2515,6 +2606,12 @@ "node": ">=8.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/package.json b/package.json index d3e9b04..b26b24f 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,8 @@ "postcss-import": "^16.1.1", "postcss-import-url": "^7.2.0", "postcss-url": "^10.1.3" + }, + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0" } -} \ No newline at end of file +} diff --git a/web/build.sh b/web/build.sh index 26583fe..84aa654 100755 --- a/web/build.sh +++ b/web/build.sh @@ -8,7 +8,7 @@ LICENSE='/* @licstart The following is the entire license notice for the JavaScript code in this page. -Copyright (c) 2025 Xe Iaso +Copyright (c) 2025 Xe Iaso Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -28,6 +28,9 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Includes code from https://github.com/aws/aws-sdk-js-crypto-helpers which is +used under the terms of the Apache 2 license. + @licend The above is the entire license notice for the JavaScript code in this page. */' @@ -36,9 +39,9 @@ for the JavaScript code in this page. mkdir -p static/locales cp ../lib/localization/locales/*.json static/locales/ -esbuild js/main.mjs --sourcemap --bundle --minify --outfile=static/js/main.mjs "--banner:js=${LICENSE}" -gzip -f -k -n static/js/main.mjs -zstd -f -k --ultra -22 static/js/main.mjs -brotli -fZk static/js/main.mjs - -esbuild js/bench.mjs --sourcemap --bundle --minify --outfile=static/js/bench.mjs +for file in js/*.mjs js/worker/*.mjs; do + esbuild "${file}" --sourcemap --bundle --minify --outfile=static/"${file}" --banner:js="${LICENSE}" + gzip -f -k -n static/${file} + zstd -f -k --ultra -22 static/${file} + brotli -fZk static/${file} +done diff --git a/web/js/algorithms/fast.mjs b/web/js/algorithms/fast.mjs new file mode 100644 index 0000000..4175e35 --- /dev/null +++ b/web/js/algorithms/fast.mjs @@ -0,0 +1,77 @@ +export default function process( + { basePrefix, version }, + data, + difficulty = 5, + signal = null, + progressCallback = null, + threads = Math.max(navigator.hardwareConcurrency / 2, 1), +) { + console.debug("fast algo"); + + let workerMethod = window.crypto !== undefined ? "webcrypto" : "purejs"; + + if (navigator.userAgent.includes("Firefox") || navigator.userAgent.includes("Goanna")) { + console.log("Firefox detected, using pure-JS fallback"); + workerMethod = "purejs"; + } + + return new Promise((resolve, reject) => { + let webWorkerURL = `${basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${version}`; + + console.log(webWorkerURL); + + const workers = []; + let settled = false; + + const cleanup = () => { + if (settled) { + return; + } + settled = true; + workers.forEach((w) => w.terminate()); + if (signal != null) { + signal.removeEventListener("abort", onAbort); + } + }; + + const onAbort = () => { + console.log("PoW aborted"); + cleanup(); + reject(new DOMException("Aborted", "AbortError")); + }; + + if (signal != null) { + if (signal.aborted) { + return onAbort(); + } + signal.addEventListener("abort", onAbort, { once: true }); + } + + for (let i = 0; i < threads; i++) { + let worker = new Worker(webWorkerURL); + + worker.onmessage = (event) => { + if (typeof event.data === "number") { + progressCallback?.(event.data); + } else { + cleanup(); + resolve(event.data); + } + }; + + worker.onerror = (event) => { + cleanup(); + reject(event); + }; + + worker.postMessage({ + data, + difficulty, + nonce: i, + threads, + }); + + workers.push(worker); + } + }); +} \ No newline at end of file diff --git a/web/js/algorithms/index.mjs b/web/js/algorithms/index.mjs new file mode 100644 index 0000000..cc1ae5d --- /dev/null +++ b/web/js/algorithms/index.mjs @@ -0,0 +1,6 @@ +import fast from "./fast.mjs"; + +export default { + fast: fast, + slow: fast, // XXX(Xe): slow is deprecated, but keep this around in case anything goes bad +} \ No newline at end of file diff --git a/web/js/bench.mjs b/web/js/bench.mjs index ba41064..a3c4f09 100644 --- a/web/js/bench.mjs +++ b/web/js/bench.mjs @@ -1,11 +1,6 @@ -import processFast from "./proof-of-work.mjs"; -import processSlow from "./proof-of-work-slow.mjs"; +import algorithms from "./algorithms/index.mjs"; const defaultDifficulty = 4; -const algorithms = { - fast: processFast, - slow: processSlow, -}; const status = document.getElementById("status"); const difficultyInput = document.getElementById("difficulty-input"); @@ -42,7 +37,7 @@ const benchmarkTrial = async (stats, difficulty, algorithm, signal) => { .join(""); const t0 = performance.now(); - const { hash, nonce } = await process(challenge, Number(difficulty), signal); + const { hash, nonce } = await process({ basePrefix: "/", version: "devel" }, challenge, Number(difficulty), signal); const t1 = performance.now(); console.log({ hash, nonce }); diff --git a/web/js/main.mjs b/web/js/main.mjs index 44bdfbe..d77a6ff 100644 --- a/web/js/main.mjs +++ b/web/js/main.mjs @@ -1,10 +1,4 @@ -import processFast from "./proof-of-work.mjs"; -import processSlow from "./proof-of-work-slow.mjs"; - -const algorithms = { - fast: processFast, - slow: processSlow, -}; +import algorithms from "./algorithms/index.mjs"; // from Xeact const u = (url = "", params = {}) => { @@ -75,11 +69,6 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key; await initTranslations(); const dependencies = [ - { - name: "WebCrypto", - msg: t('web_crypto_error'), - value: window.crypto, - }, { name: "Web Workers", msg: t('web_workers_error'), @@ -119,15 +108,6 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key; progress.style.display = "none"; }; - if (!window.isSecureContext) { - ohNoes({ - titleMsg: t('context_not_secure'), - statusMsg: t('context_not_secure_msg'), - imageSrc: imageURL("reject", anubisVersion, basePrefix), - }); - return; - } - status.innerHTML = t('calculating'); for (const { value, name, msg } of dependencies) { @@ -171,6 +151,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key; try { const t0 = Date.now(); const { hash, nonce } = await process( + { basePrefix, version: anubisVersion }, challenge, rules.difficulty, null, diff --git a/web/js/proof-of-work-slow.mjs b/web/js/proof-of-work-slow.mjs deleted file mode 100644 index 33780f8..0000000 --- a/web/js/proof-of-work-slow.mjs +++ /dev/null @@ -1,99 +0,0 @@ -// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm - -export default function process( - data, - difficulty = 5, - signal = null, - progressCallback = null, - _threads = 1, -) { - console.debug("slow algo"); - return new Promise((resolve, reject) => { - let webWorkerURL = URL.createObjectURL( - new Blob(["(", processTask(), ")()"], { type: "application/javascript" }), - ); - - let worker = new Worker(webWorkerURL); - let settled = false; - - const cleanup = () => { - if (settled) return; - settled = true; - worker.terminate(); - if (signal != null) { - signal.removeEventListener("abort", onAbort); - } - URL.revokeObjectURL(webWorkerURL); - }; - - const onAbort = () => { - console.log("PoW aborted"); - cleanup(); - reject(new DOMException("Aborted", "AbortError")); - }; - - if (signal != null) { - if (signal.aborted) { - return onAbort(); - } - signal.addEventListener("abort", onAbort, { once: true }); - } - - worker.onmessage = (event) => { - if (typeof event.data === "number") { - progressCallback?.(event.data); - } else { - cleanup(); - resolve(event.data); - } - }; - - worker.onerror = (event) => { - cleanup(); - reject(event); - }; - - worker.postMessage({ - data, - difficulty, - }); - }); -} - -function processTask() { - return function () { - const sha256 = (text) => { - const encoded = new TextEncoder().encode(text); - return crypto.subtle.digest("SHA-256", encoded.buffer).then((result) => - Array.from(new Uint8Array(result)) - .map((c) => c.toString(16).padStart(2, "0")) - .join(""), - ); - }; - - addEventListener("message", async (event) => { - let data = event.data.data; - let difficulty = event.data.difficulty; - - let hash; - let nonce = 0; - do { - if ((nonce & 1023) === 0) { - postMessage(nonce); - } - hash = await sha256(data + nonce++); - } while ( - hash.substring(0, difficulty) !== Array(difficulty + 1).join("0") - ); - - nonce -= 1; // last nonce was post-incremented - - postMessage({ - hash, - data, - difficulty, - nonce, - }); - }); - }.toString(); -} diff --git a/web/js/proof-of-work.mjs b/web/js/proof-of-work.mjs deleted file mode 100644 index d70b9ee..0000000 --- a/web/js/proof-of-work.mjs +++ /dev/null @@ -1,137 +0,0 @@ -export default function process( - data, - difficulty = 5, - signal = null, - progressCallback = null, - threads = Math.max(navigator.hardwareConcurrency / 2, 1), -) { - console.debug("fast algo"); - return new Promise((resolve, reject) => { - let webWorkerURL = URL.createObjectURL( - new Blob(["(", processTask(), ")()"], { type: "application/javascript" }), - ); - - const workers = []; - let settled = false; - - const cleanup = () => { - if (settled) { - return; - } - settled = true; - workers.forEach((w) => w.terminate()); - if (signal != null) { - signal.removeEventListener("abort", onAbort); - } - URL.revokeObjectURL(webWorkerURL); - }; - - const onAbort = () => { - console.log("PoW aborted"); - cleanup(); - reject(new DOMException("Aborted", "AbortError")); - }; - - if (signal != null) { - if (signal.aborted) { - return onAbort(); - } - signal.addEventListener("abort", onAbort, { once: true }); - } - - for (let i = 0; i < threads; i++) { - let worker = new Worker(webWorkerURL); - - worker.onmessage = (event) => { - if (typeof event.data === "number") { - progressCallback?.(event.data); - } else { - cleanup(); - resolve(event.data); - } - }; - - worker.onerror = (event) => { - cleanup(); - reject(event); - }; - - worker.postMessage({ - data, - difficulty, - nonce: i, - threads, - }); - - workers.push(worker); - } - }); -} - -function processTask() { - return function () { - const sha256 = (text) => { - const encoded = new TextEncoder().encode(text); - return crypto.subtle.digest("SHA-256", encoded.buffer); - }; - - function uint8ArrayToHexString(arr) { - return Array.from(arr) - .map((c) => c.toString(16).padStart(2, "0")) - .join(""); - } - - addEventListener("message", async (event) => { - let data = event.data.data; - let difficulty = event.data.difficulty; - let hash; - let nonce = event.data.nonce; - let threads = event.data.threads; - - const threadId = nonce; - let localIterationCount = 0; - - while (true) { - const currentHash = await sha256(data + nonce); - const thisHash = new Uint8Array(currentHash); - let valid = true; - - for (let j = 0; j < difficulty; j++) { - const byteIndex = Math.floor(j / 2); // which byte we are looking at - const nibbleIndex = j % 2; // which nibble in the byte we are looking at (0 is high, 1 is low) - - let nibble = - (thisHash[byteIndex] >> (nibbleIndex === 0 ? 4 : 0)) & 0x0f; // Get the nibble - - if (nibble !== 0) { - valid = false; - break; - } - } - - if (valid) { - hash = uint8ArrayToHexString(thisHash); - console.log(hash); - break; - } - - nonce += threads; - - // send a progress update every 1024 iterations so that the user can be informed of - // the state of the challenge. - if (threadId == 0 && localIterationCount === 1024) { - postMessage(nonce); - localIterationCount = 0; - } - localIterationCount++; - } - - postMessage({ - hash, - data, - difficulty, - nonce, - }); - }); - }.toString(); -} diff --git a/web/js/video.mjs b/web/js/video.mjs deleted file mode 100644 index 0aa7a78..0000000 --- a/web/js/video.mjs +++ /dev/null @@ -1,16 +0,0 @@ -const videoElement = ``; - -export const testVideo = async (testarea) => { - testarea.innerHTML = videoElement; - return await new Promise((resolve) => { - const video = document.getElementById("videotest"); - video.oncanplay = () => { - testarea.style.display = "none"; - resolve(true); - }; - video.onerror = () => { - testarea.style.display = "none"; - resolve(false); - }; - }); -}; diff --git a/web/js/worker/sha256-purejs.mjs b/web/js/worker/sha256-purejs.mjs new file mode 100644 index 0000000..60d9971 --- /dev/null +++ b/web/js/worker/sha256-purejs.mjs @@ -0,0 +1,61 @@ +import { Sha256 } from '@aws-crypto/sha256-js'; + +const calculateSHA256 = (text) => { + const hash = new Sha256(); + hash.update(text); + return hash.digest(); +}; + +function uint8ArrayToHexString(arr) { + return Array.from(arr) + .map((c) => c.toString(16).padStart(2, "0")) + .join(""); +} + +addEventListener('message', async ({ data: eventData }) => { + const { data, difficulty, threads } = eventData; + let nonce = eventData.nonce; + const isMainThread = nonce === 0; + let iterations = 0; + + const requiredZeroBytes = Math.floor(difficulty / 2); + const isDifficultyOdd = difficulty % 2 !== 0; + + for (; ;) { + const hashBuffer = await calculateSHA256(data + nonce); + const hashArray = new Uint8Array(hashBuffer); + + let isValid = true; + for (let i = 0; i < requiredZeroBytes; i++) { + if (hashArray[i] !== 0) { + isValid = false; + break; + } + } + + if (isValid && isDifficultyOdd) { + if ((hashArray[requiredZeroBytes] >> 4) !== 0) { + isValid = false; + } + } + + if (isValid) { + const finalHash = toHexString(hashArray); + postMessage({ + hash: finalHash, + data, + difficulty, + nonce, + }); + return; // Exit worker + } + + nonce += threads; + iterations++; + + // Send a progress update from the main thread every 1024 iterations. + if (isMainThread && (iterations & 1023) === 0) { + postMessage(nonce); + } + } +}); \ No newline at end of file diff --git a/web/js/worker/sha256-webcrypto.mjs b/web/js/worker/sha256-webcrypto.mjs new file mode 100644 index 0000000..97466fb --- /dev/null +++ b/web/js/worker/sha256-webcrypto.mjs @@ -0,0 +1,57 @@ +const encoder = new TextEncoder(); +const calculateSHA256 = async (input) => { + const data = encoder.encode(input); + return await crypto.subtle.digest("SHA-256", data); +}; + +const toHexString = (byteArray) => { + return byteArray.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); +}; + +addEventListener("message", async ({ data: eventData }) => { + const { data, difficulty, threads } = eventData; + let nonce = eventData.nonce; + const isMainThread = nonce === 0; + let iterations = 0; + + const requiredZeroBytes = Math.floor(difficulty / 2); + const isDifficultyOdd = difficulty % 2 !== 0; + + for (; ;) { + const hashBuffer = await calculateSHA256(data + nonce); + const hashArray = new Uint8Array(hashBuffer); + + let isValid = true; + for (let i = 0; i < requiredZeroBytes; i++) { + if (hashArray[i] !== 0) { + isValid = false; + break; + } + } + + if (isValid && isDifficultyOdd) { + if ((hashArray[requiredZeroBytes] >> 4) !== 0) { + isValid = false; + } + } + + if (isValid) { + const finalHash = toHexString(hashArray); + postMessage({ + hash: finalHash, + data, + difficulty, + nonce, + }); + return; // Exit worker + } + + nonce += threads; + iterations++; + + // Send a progress update from the main thread every 1024 iterations. + if (isMainThread && (iterations & 1023) === 0) { + postMessage(nonce); + } + } +}); \ No newline at end of file