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