cmd/anubis: drastically optimize proof of work (#19)

* cmd/anubis: drastically optimize proof of work

Closes #12
Closes #17

This drastically optimizes the proof of work check by removing the
stringify call at every iteration. Additionally, this optimizes the
checks by running them in parallel for as many threads as the browser
has available (according to navigator.hardwareConcurrency).

This also changes the redirect lag to 250 milliseconds instead of 2000
milliseconds in order to be perceptually faster. This is below the
reaction time threshold of many people, so this will make the post-check
success phase perceptually instant.

Testing on an iPhone 7 Plus has shown that this can clear a difficulty 4
check in 3.4 seconds.

This actually optimizes the check so much it may be a logistical concern
for operators.

* cmd/anubis/js: fix happy cachebuster logic

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
Xe Iaso 2025-03-20 09:26:39 -04:00 committed by GitHub
parent c81e938f63
commit 52d7a3cd2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 166 additions and 113 deletions

View file

@ -11,15 +11,16 @@ const u = (url = "", params = {}) => {
return result.toString();
};
const imageURL = (mood) => {
return `/.within.website/x/cmd/anubis/static/img/${mood}.webp`;
};
const imageURL = (mood, cacheBuster) =>
u(`/.within.website/x/cmd/anubis/static/img/${mood}.webp`, { cacheBuster });
(async () => {
const status = document.getElementById('status');
const image = document.getElementById('image');
const title = document.getElementById('title');
const spinner = document.getElementById('spinner');
const anubisVersion = JSON.parse(document.getElementById('anubis_version').textContent);
// const testarea = document.getElementById('testarea');
// const videoWorks = await testVideo(testarea);
@ -57,15 +58,16 @@ const imageURL = (mood) => {
const t0 = Date.now();
const { hash, nonce } = await process(challenge, difficulty);
const t1 = Date.now();
console.log({ hash, nonce });
title.innerHTML = "Success!";
status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`;
image.src = imageURL("happy");
image.src = imageURL("happy", anubisVersion);
spinner.innerHTML = "";
spinner.style.display = "none";
setTimeout(() => {
const redir = window.location.href;
window.location.href = u("/.within.website/x/cmd/anubis/api/pass-challenge", { response: hash, nonce, redir, elapsedTime: t1 - t0 });
}, 2000);
}, 250);
})();

View file

@ -1,27 +1,35 @@
// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm
export function process(data, difficulty = 5) {
export function process(data, difficulty = 5, threads = navigator.hardwareConcurrency) {
return new Promise((resolve, reject) => {
let webWorkerURL = URL.createObjectURL(new Blob([
'(', processTask(), ')()'
], { type: 'application/javascript' }));
let worker = new Worker(webWorkerURL);
const workers = [];
worker.onmessage = (event) => {
worker.terminate();
resolve(event.data);
};
for (let i = 0; i < threads; i++) {
let worker = new Worker(webWorkerURL);
worker.onerror = (event) => {
worker.terminate();
reject();
};
worker.onmessage = (event) => {
workers.forEach(worker => worker.terminate());
worker.terminate();
resolve(event.data);
};
worker.postMessage({
data,
difficulty
});
worker.onerror = (event) => {
worker.terminate();
reject();
};
worker.postMessage({
data,
difficulty,
nonce: 1000000 * i,
});
workers.push(worker);
}
URL.revokeObjectURL(webWorkerURL);
});
@ -31,22 +39,44 @@ 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(""),
);
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 = 0;
do {
hash = await sha256(data + nonce++);
} while (hash.substring(0, difficulty) !== Array(difficulty + 1).join('0'));
let nonce = event.data.nonce || 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 -= 1; // last nonce was post-incremented