mirror of
https://github.com/TecharoHQ/anubis.git
synced 2025-08-03 01:38:14 -04:00
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 <me@xeiaso.net>
This commit is contained in:
parent
b499aaab68
commit
8f4f521312
97
package-lock.json
generated
97
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*/'
|
||||
|
77
web/js/algorithms/fast.mjs
Normal file
77
web/js/algorithms/fast.mjs
Normal file
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
6
web/js/algorithms/index.mjs
Normal file
6
web/js/algorithms/index.mjs
Normal file
@ -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
|
||||
}
|
@ -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 });
|
||||
|
||||
|
@ -1,9 +1,4 @@
|
||||
import process from "./proof-of-work.mjs";
|
||||
|
||||
const algorithms = {
|
||||
fast: process,
|
||||
slow: process,
|
||||
};
|
||||
import algorithms from "./algorithms/index.mjs";
|
||||
|
||||
// from Xeact
|
||||
const u = (url = "", params = {}) => {
|
||||
@ -170,6 +165,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,
|
||||
|
@ -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();
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
const videoElement = `<video id="videotest" width="0" height="0" src="/.within.website/x/cmd/anubis/static/testdata/black.mp4"></video>`;
|
||||
|
||||
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);
|
||||
};
|
||||
});
|
||||
};
|
61
web/js/worker/sha256-purejs.mjs
Normal file
61
web/js/worker/sha256-purejs.mjs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
57
web/js/worker/sha256-webcrypto.mjs
Normal file
57
web/js/worker/sha256-webcrypto.mjs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user