From 52d7a3cd2b9b551b0e5e8d95a43ee8408484c133 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Thu, 20 Mar 2025 09:26:39 -0400 Subject: [PATCH] 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 --------- Signed-off-by: Xe Iaso --- cmd/anubis/index.templ | 135 ++++++++++++++++-------------- cmd/anubis/index_templ.go | 44 ++++++---- cmd/anubis/js/main.mjs | 12 +-- cmd/anubis/js/proof-of-work.mjs | 78 +++++++++++------ cmd/anubis/main.go | 2 +- cmd/anubis/static/js/main.mjs | 2 +- cmd/anubis/static/js/main.mjs.br | Bin 802 -> 993 bytes cmd/anubis/static/js/main.mjs.gz | Bin 985 -> 1193 bytes cmd/anubis/static/js/main.mjs.map | 6 +- cmd/anubis/static/js/main.mjs.zst | Bin 982 -> 1191 bytes 10 files changed, 166 insertions(+), 113 deletions(-) diff --git a/cmd/anubis/index.templ b/cmd/anubis/index.templ index 028e89e..a2270fa 100644 --- a/cmd/anubis/index.templ +++ b/cmd/anubis/index.templ @@ -1,19 +1,18 @@ package main import ( -"github.com/TecharoHQ/anubis" -"github.com/TecharoHQ/anubis/xess" + "github.com/TecharoHQ/anubis" + "github.com/TecharoHQ/anubis/xess" ) templ base(title string, body templ.Component) { - - - - - { title } - - - - - - -
-
-

{ title }

-
- @body - -
- - - + + @templ.JSONScript("anubis_version", anubis.Version) + + +
+
+

{ title }

+
+ @body + +
+ + } templ index() { -
- Loading...

- +
+
+
+
+
+
+
+
+
+
+ +
+
} templ errorPage(message string) { -
- Try again -

Go home

-
-} \ No newline at end of file +
+ Try again +

Go home

+
+} diff --git a/cmd/anubis/index_templ.go b/cmd/anubis/index_templ.go index 70bd028..9bfa351 100644 --- a/cmd/anubis/index_templ.go +++ b/cmd/anubis/index_templ.go @@ -41,7 +41,7 @@ func base(title string, body templ.Component) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 13, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 12, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -54,26 +54,34 @@ func base(title string, body templ.Component) templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(xess.URL) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 14, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 13, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.JSONScript("anubis_version", anubis.Version).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 148, Col: 50} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 147, Col: 49} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -81,7 +89,7 @@ func base(title string, body templ.Component) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -110,7 +118,7 @@ func index() templ.Component { templ_7745c5c3_Var5 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

Loading...

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -180,33 +188,33 @@ func errorPage(message string) templ.Component { templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(message) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 194, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 207, Col: 14} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, ".

Go home

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ".

Go home

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/cmd/anubis/js/main.mjs b/cmd/anubis/js/main.mjs index 3f2c652..fc85a44 100644 --- a/cmd/anubis/js/main.mjs +++ b/cmd/anubis/js/main.mjs @@ -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); })(); \ No newline at end of file diff --git a/cmd/anubis/js/proof-of-work.mjs b/cmd/anubis/js/proof-of-work.mjs index d71d2db..bd69c71 100644 --- a/cmd/anubis/js/proof-of-work.mjs +++ b/cmd/anubis/js/proof-of-work.mjs @@ -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 diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index 45afeb2..c71d368 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -82,7 +82,7 @@ const ( ) //go:generate go tool github.com/a-h/templ/cmd/templ generate -//go:generate esbuild js/main.mjs --sourcemap --minify --bundle --outfile=static/js/main.mjs +//go:generate esbuild js/main.mjs --sourcemap --bundle --minify --outfile=static/js/main.mjs //go:generate gzip -f -k static/js/main.mjs //go:generate zstd -f -k --ultra -22 static/js/main.mjs //go:generate brotli -fZk static/js/main.mjs diff --git a/cmd/anubis/static/js/main.mjs b/cmd/anubis/static/js/main.mjs index 043f617..3d9e2d0 100644 --- a/cmd/anubis/static/js/main.mjs +++ b/cmd/anubis/static/js/main.mjs @@ -1,2 +1,2 @@ -(()=>{function l(n,s=5){return new Promise((i,e)=>{let o=URL.createObjectURL(new Blob(["(",w(),")()"],{type:"application/javascript"})),t=new Worker(o);t.onmessage=r=>{t.terminate(),i(r.data)},t.onerror=r=>{t.terminate(),e()},t.postMessage({data:n,difficulty:s}),URL.revokeObjectURL(o)})}function w(){return function(){let n=s=>{let i=new TextEncoder().encode(s);return crypto.subtle.digest("SHA-256",i.buffer).then(e=>Array.from(new Uint8Array(e)).map(o=>o.toString(16).padStart(2,"0")).join(""))};addEventListener("message",async s=>{let i=s.data.data,e=s.data.difficulty,o,t=0;do o=await n(i+t++);while(o.substring(0,e)!==Array(e+1).join("0"));t-=1,postMessage({hash:o,data:i,difficulty:e,nonce:t})})}.toString()}var h=(n="",s={})=>{let i=new URL(n,window.location.href);return Object.entries(s).forEach(e=>{let[o,t]=e;i.searchParams.set(o,t)}),i.toString()},m=n=>`/.within.website/x/cmd/anubis/static/img/${n}.webp`;(async()=>{let n=document.getElementById("status"),s=document.getElementById("image"),i=document.getElementById("title"),e=document.getElementById("spinner");n.innerHTML="Calculating...";let{challenge:o,difficulty:t}=await fetch("/.within.website/x/cmd/anubis/api/make-challenge",{method:"POST"}).then(a=>{if(!a.ok)throw new Error("Failed to fetch config");return a.json()}).catch(a=>{throw i.innerHTML="Oh no!",n.innerHTML=`Failed to fetch config: ${a.message}`,s.src=m("sad"),e.innerHTML="",e.style.display="none",a});n.innerHTML=`Calculating...
Difficulty: ${t}`;let r=Date.now(),{hash:u,nonce:c}=await l(o,t),d=Date.now();i.innerHTML="Success!",n.innerHTML=`Done! Took ${d-r}ms, ${c} iterations`,s.src=m("happy"),e.innerHTML="",e.style.display="none",setTimeout(()=>{let a=window.location.href;window.location.href=h("/.within.website/x/cmd/anubis/api/pass-challenge",{response:u,nonce:c,redir:a,elapsedTime:d-r})},2e3)})();})(); +(()=>{function m(n,i=5,e=navigator.hardwareConcurrency){return new Promise((t,d)=>{let s=URL.createObjectURL(new Blob(["(",p(),")()"],{type:"application/javascript"})),a=[];for(let u=0;u{a.forEach(c=>c.terminate()),o.terminate(),t(r.data)},o.onerror=r=>{o.terminate(),d()},o.postMessage({data:n,difficulty:i,nonce:1e6*u}),a.push(o)}URL.revokeObjectURL(s)})}function p(){return function(){let n=e=>{let t=new TextEncoder().encode(e);return crypto.subtle.digest("SHA-256",t.buffer)};function i(e){return Array.from(e).map(t=>t.toString(16).padStart(2,"0")).join("")}addEventListener("message",async e=>{let t=e.data.data,d=e.data.difficulty,s,a=e.data.nonce||0;for(;;){let u=await n(t+a++),o=new Uint8Array(u),r=!0;for(let c=0;c>(f===0?4:0)&15)!==0){r=!1;break}}if(r){s=i(o),console.log(s);break}}a-=1,postMessage({hash:s,data:t,difficulty:d,nonce:a})})}.toString()}var w=(n="",i={})=>{let e=new URL(n,window.location.href);return Object.entries(i).forEach(t=>{let[d,s]=t;e.searchParams.set(d,s)}),e.toString()},h=(n,i)=>w(`/.within.website/x/cmd/anubis/static/img/${n}.webp`,{cacheBuster:i});(async()=>{let n=document.getElementById("status"),i=document.getElementById("image"),e=document.getElementById("title"),t=document.getElementById("spinner"),d=JSON.parse(document.getElementById("anubis_version").textContent);n.innerHTML="Calculating...";let{challenge:s,difficulty:a}=await fetch("/.within.website/x/cmd/anubis/api/make-challenge",{method:"POST"}).then(l=>{if(!l.ok)throw new Error("Failed to fetch config");return l.json()}).catch(l=>{throw e.innerHTML="Oh no!",n.innerHTML=`Failed to fetch config: ${l.message}`,i.src=h("sad"),t.innerHTML="",t.style.display="none",l});n.innerHTML=`Calculating...
Difficulty: ${a}`;let u=Date.now(),{hash:o,nonce:r}=await m(s,a),c=Date.now();console.log({hash:o,nonce:r}),e.innerHTML="Success!",n.innerHTML=`Done! Took ${c-u}ms, ${r} iterations`,i.src=h("happy",d),t.innerHTML="",t.style.display="none",setTimeout(()=>{let l=window.location.href;window.location.href=w("/.within.website/x/cmd/anubis/api/pass-challenge",{response:o,nonce:r,redir:l,elapsedTime:c-u})},250)})();})(); //# sourceMappingURL=main.mjs.map diff --git a/cmd/anubis/static/js/main.mjs.br b/cmd/anubis/static/js/main.mjs.br index 27625da8d8e1ac10fbaf5f0c90388dc900210253..56b664638301a24f1977a8405d30958ce3469a62 100644 GIT binary patch literal 993 zcmV<710MXrP(}b?q;$8UQ2Kx6?4HX>+4leXWz_AGpY+?7dP>C^jBF6*0h2~3B(Ul4 zRnEeI2PYr=rUxHCJaS-}MI=4z0P%H*5W|xL&(yGM)G(P~l=i?6*nQIrCPG3LWboOn<;Z(Q zOYhJwa^;ryTG>O&W!V5Cg`e`2AxiStu>MJ8APR=XsiOl&kn#j*b|_wtEj?i}zO>}$ z@n(2#KE53)lE$1rV5N|1GW-aLBW{8gkZQcU|^VU!;WyD9twy z7;ye!8z1`|WExi48QIwzlQM`u4yTI`9|dK(8rEwQv8wnZ7R(+Q#1EqCRWK;Gx`)hr z*|6QsgF>+|P~Ou&E?c|3y>rxmxmoocr`=CdI;)5c*9jlmn^`61A5M;Sj<};X#BZga(cwGYxj6JfndU!0di7^K^ zde%|8;H*qF3SO@JV##FqcLpnxNtBH?C*}lB#e2AJZRDcp8;J!SoL_9Hb=R~p>BqqU5K_kjGI|Z{R_0tlZ z7f2mzDP%^I`T4=QHeJbNzd$L3iB!*^^3pOvwEYcZQVMmnlzBH=k)Ywy74NsPHo4t^ zAbDK(wow7A8Q0$6cEK~Ru5q!lK!kNnt-d>9L&z&b|IvK+lEPKu30y(U{B!#o}lNp}J%pH2~! zr+oKUp-Vi;{w*J~wKpiK>%e7FZBPBjItG{<6ia0`hvQtNz$;(b=H z%x0P(WN3V>N7wd(IX=noMz(R P@2_0(?BD)Rx=FtS;wRhc literal 802 zcmV+-1Ks?w7&`zV+fwU)pO>R+(nP^qsy(_=%Bz~h%7qm009!o_9WD(dkjTPCkm#-U zUK~i6wtvurK2d9Q&hDvvuvrcw$)gb}dyAfv9P3hx*Q?n%4k9{M_&Zz8Rgj5d_J=!%A+xx zo=A$bBC*40%3!3zu&AW1AXSPm5Iuw&lsJamYdkYfut|luK%7x8!zLD}P(hCF3h`1e zun-*buDvvzU2g!7F|c*!9xF{m zGOwNPXA9==Y@27Y&!mF$7NiPHF$QN;_Htuvav@P&$(2+j_w$vY7XVH&7NQK|(UHvl zJnCTbON<6j#zbZrqBrft8e8SB!1<48>&!O(tMh%7G-KdQh1lh(oOj~oQCAynE-^y+ zOMP80c71LA#oVP@_@SZ=*7rtV;9Nu3A~mQ;Yk4sTSddQy;&ehP0&LiHwCwVhP1U7g zI(lP3tv4R^srMUUjWN(}HIMTgG!V$A$%R$)OUB-`V;A?72%I?YB#AG|;gDadBhAv-L4ae}EDbLl8x ztfKgWQ0apj#?mCYkR0iP3ZgC7&T|hv*p`?KC$8x}_#$|n4CbQn-q_D#*(E-tE=uLH zM#1V&D`Z)_v#+;VJoaB{MQ%s7-%l=V1>06H4yVzLPtsRo~`4TqeeL#f| g^pFbSbf=r^`fd+)WH67epWi-6MV)cCP8wQx2lH);%m4rY diff --git a/cmd/anubis/static/js/main.mjs.gz b/cmd/anubis/static/js/main.mjs.gz index c80b596f23815605234730d4012386a6cef6f863..f4665eedfeaa9e2e48d7601f8549893588b60044 100644 GIT binary patch literal 1193 zcmV;a1XlYWiwFp>I@@Oe18rexZZ2(Va{!H1?{C{S5dGf2LgBiBTp7vR! z8IxkA)^M&&8)>++p4^}VP%0q0uO)M;ukKWZL0Hu-VeX5A`60#Qu$&G7jSKN?tV37*wW;N~DGH<8DlVsnTjUI;2k`RO zwnGr2X+udJ>w0!P+`GFSb^_~Ix19vCvmtr&j*eihgf+`?fPD#{)YBdo9IMnXR!j2n>mC?$;F4)`-ev_36Emd76o|LW!u^YuWyRK_FgSTfiuRJqE-!! z@;Hj06|;Ez1*8@ zWu#F?`c?co8L zl6wc)47GmObr|Z|DoBG<=Gxidko3yUdsVJO)V@66kCaoYP)-9M1Qd4#l&=M(x?UR( zq_gf``D7ufm4x8JuDZ?Q!3ervHoh=6cMGgOu9PS}6kA!0xUsKnWxGD8x#25~#o{&jKwH%i~5^?fsbMEu`9_<$223{B*7 zL?gvzh%B>W$oAp#>{ODss=}coGj=Y7AQ@s_=~7h{*g5p-c#wta)>M9Wh%&%=*F@M zs=F|AvQ<mpSxIBop>K-$F^O)&ZGoK#6dmtA}s_rHmE#)2BJ=)CzEq~ln zx86v<89`0am~hp1LbKhy|N2_LHalHkv|1y)ujzP)o$n==&fQ@l}9IDDlg=B>4ILK}v9mFHH@ujdZ(Tm3+!CvZr3k#gAyNQ6W6ioQKe0-|)Z&_mCSu ztvFPw36S?qO?$ZLCx=H9loGNz{5(t3pOWBOui*^A;^@%hWUa7)`Zo06rDy*GjcZ0a H%Lo7fl_X%F literal 985 zcmV;~119_*iwFp-%GhTB18rexZZ2(Va{!H0UvJws5P$EdP`Eyj2ty?qS`5}0pj|Sw zK!OBwiaitsOj9R|i9`w{71uEI-FK87#~#w64;D!Jc)Z`AJ4%_nSXaH((Kwqm)bb$T zv9*WTdz)EUW_RATCIF>|LkeynW=?+j=UQkFD#Gpj0d&MYP0=44H>Y0-C45O4CyX-k zjj!XXg9%Yx*BGtROZkI(QbBvuMKUnPqfD3ob^Z}Nb*zZO*%m@j3y>ZkjUs|?jm7sc zt)X6&Dk?Vc6bRls|4JDCQb6ZIyxHWU>vU~md1HWwk322@^1`Xkf+mw)U$oIuZJLY(SJ|0U=T{{q*7F z{M~!PjhOdU1)hmmgQXxZK6tNIqCyo$g1;CW{}|6GFeX~nQ712)h;ACau?u?ko{3JC z)2Mu;=bW4p40~|KQi7|YP-S`b1U6oq5COTPWJ@&RDy*!|Uf_h0Yf;|Ds`D%2ClN4d|x}|8`#)hKyQ0=7IXpfR%MhK{x zsGPr2x=u2rR9_MBHwlFi0hHJEo${&;xQP`1!Fo`>y@Gfvt-P4!VrgP+tXRT4mq+qE51P&X^+CCABRMPcEk|)$xEHF=oi8YE5_BQ`4qKm85Rp~#b7oX$@>V^<$u1k=>lR||$_cr{_2+@V<+947F-|c&ihKf*0jZIhw_&^)u|8;Jmn#DST($m#66V8 zPjKorstZu2q9zF}HuX9Dj#fdo#?PBP|1Ary_Zn^xDoTTcMz#tIXdglv&u{(%@gJ}0 HWCs8MzQ^Ee diff --git a/cmd/anubis/static/js/main.mjs.map b/cmd/anubis/static/js/main.mjs.map index d5b288f..577503b 100644 --- a/cmd/anubis/static/js/main.mjs.map +++ b/cmd/anubis/static/js/main.mjs.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../js/proof-of-work.mjs", "../../js/main.mjs"], - "sourcesContent": ["// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm\n\nexport function process(data, difficulty = 5) {\n return new Promise((resolve, reject) => {\n let webWorkerURL = URL.createObjectURL(new Blob([\n '(', processTask(), ')()'\n ], { type: 'application/javascript' }));\n\n let worker = new Worker(webWorkerURL);\n\n worker.onmessage = (event) => {\n worker.terminate();\n resolve(event.data);\n };\n\n worker.onerror = (event) => {\n worker.terminate();\n reject();\n };\n\n worker.postMessage({\n data,\n difficulty\n });\n\n URL.revokeObjectURL(webWorkerURL);\n });\n}\n\nfunction processTask() {\n return function () {\n const sha256 = (text) => {\n const encoded = new TextEncoder().encode(text);\n return crypto.subtle.digest(\"SHA-256\", encoded.buffer).then((result) =>\n Array.from(new Uint8Array(result))\n .map((c) => c.toString(16).padStart(2, \"0\"))\n .join(\"\"),\n );\n };\n\n addEventListener('message', async (event) => {\n let data = event.data.data;\n let difficulty = event.data.difficulty;\n\n let hash;\n let nonce = 0;\n do {\n hash = await sha256(data + nonce++);\n } while (hash.substring(0, difficulty) !== Array(difficulty + 1).join('0'));\n\n nonce -= 1; // last nonce was post-incremented\n\n postMessage({\n hash,\n data,\n difficulty,\n nonce,\n });\n });\n }.toString();\n}\n\n", "import { process } from './proof-of-work.mjs';\nimport { testVideo } from './video.mjs';\n\n// from Xeact\nconst u = (url = \"\", params = {}) => {\n let result = new URL(url, window.location.href);\n Object.entries(params).forEach((kv) => {\n let [k, v] = kv;\n result.searchParams.set(k, v);\n });\n return result.toString();\n};\n\nconst imageURL = (mood) => {\n return `/.within.website/x/cmd/anubis/static/img/${mood}.webp`;\n};\n\n(async () => {\n const status = document.getElementById('status');\n const image = document.getElementById('image');\n const title = document.getElementById('title');\n const spinner = document.getElementById('spinner');\n // const testarea = document.getElementById('testarea');\n\n // const videoWorks = await testVideo(testarea);\n // console.log(`videoWorks: ${videoWorks}`);\n\n // if (!videoWorks) {\n // title.innerHTML = \"Oh no!\";\n // status.innerHTML = \"Checks failed. Please check your browser's settings and try again.\";\n // image.src = imageURL(\"sad\");\n // spinner.innerHTML = \"\";\n // spinner.style.display = \"none\";\n // return;\n // }\n\n status.innerHTML = 'Calculating...';\n\n const { challenge, difficulty } = await fetch(\"/.within.website/x/cmd/anubis/api/make-challenge\", { method: \"POST\" })\n .then(r => {\n if (!r.ok) {\n throw new Error(\"Failed to fetch config\");\n }\n return r.json();\n })\n .catch(err => {\n title.innerHTML = \"Oh no!\";\n status.innerHTML = `Failed to fetch config: ${err.message}`;\n image.src = imageURL(\"sad\");\n spinner.innerHTML = \"\";\n spinner.style.display = \"none\";\n throw err;\n });\n\n status.innerHTML = `Calculating...
Difficulty: ${difficulty}`;\n\n const t0 = Date.now();\n const { hash, nonce } = await process(challenge, difficulty);\n const t1 = Date.now();\n\n title.innerHTML = \"Success!\";\n status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`;\n image.src = imageURL(\"happy\");\n spinner.innerHTML = \"\";\n spinner.style.display = \"none\";\n\n setTimeout(() => {\n const redir = window.location.href;\n window.location.href = u(\"/.within.website/x/cmd/anubis/api/pass-challenge\", { response: hash, nonce, redir, elapsedTime: t1 - t0 });\n }, 2000);\n})();"], - "mappings": "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", - "names": ["process", "data", "difficulty", "resolve", "reject", "webWorkerURL", "processTask", "worker", "event", "sha256", "text", "encoded", "result", "c", "hash", "nonce", "u", "url", "params", "result", "kv", "k", "v", "imageURL", "mood", "status", "image", "title", "spinner", "challenge", "difficulty", "r", "err", "t0", "hash", "nonce", "process", "t1", "redir"] + "sourcesContent": ["// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm\n\nexport function process(data, difficulty = 5, threads = navigator.hardwareConcurrency) {\n return new Promise((resolve, reject) => {\n let webWorkerURL = URL.createObjectURL(new Blob([\n '(', processTask(), ')()'\n ], { type: 'application/javascript' }));\n\n const workers = [];\n\n for (let i = 0; i < threads; i++) {\n let worker = new Worker(webWorkerURL);\n\n worker.onmessage = (event) => {\n workers.forEach(worker => worker.terminate());\n worker.terminate();\n resolve(event.data);\n };\n\n worker.onerror = (event) => {\n worker.terminate();\n reject();\n };\n\n worker.postMessage({\n data,\n difficulty,\n nonce: 1000000 * i,\n });\n\n workers.push(worker);\n }\n\n URL.revokeObjectURL(webWorkerURL);\n });\n}\n\nfunction processTask() {\n return function () {\n const sha256 = (text) => {\n const encoded = new TextEncoder().encode(text);\n return crypto.subtle.digest(\"SHA-256\", encoded.buffer);\n };\n\n function uint8ArrayToHexString(arr) {\n return Array.from(arr)\n .map((c) => c.toString(16).padStart(2, \"0\"))\n .join(\"\");\n }\n\n addEventListener('message', async (event) => {\n let data = event.data.data;\n let difficulty = event.data.difficulty;\n let hash;\n let nonce = event.data.nonce || 0;\n\n while (true) {\n const currentHash = await sha256(data + nonce++);\n const thisHash = new Uint8Array(currentHash);\n let valid = true;\n\n for (let j = 0; j < difficulty; j++) {\n const byteIndex = Math.floor(j / 2); // which byte we are looking at\n const nibbleIndex = j % 2; // which nibble in the byte we are looking at (0 is high, 1 is low)\n\n let nibble = (thisHash[byteIndex] >> (nibbleIndex === 0 ? 4 : 0)) & 0x0F; // Get the nibble\n\n if (nibble !== 0) {\n valid = false;\n break;\n }\n }\n\n if (valid) {\n hash = uint8ArrayToHexString(thisHash);\n console.log(hash);\n break;\n }\n }\n\n nonce -= 1; // last nonce was post-incremented\n\n postMessage({\n hash,\n data,\n difficulty,\n nonce,\n });\n });\n }.toString();\n}\n\n", "import { process } from './proof-of-work.mjs';\nimport { testVideo } from './video.mjs';\n\n// from Xeact\nconst u = (url = \"\", params = {}) => {\n let result = new URL(url, window.location.href);\n Object.entries(params).forEach((kv) => {\n let [k, v] = kv;\n result.searchParams.set(k, v);\n });\n return result.toString();\n};\n\nconst imageURL = (mood, cacheBuster) =>\n u(`/.within.website/x/cmd/anubis/static/img/${mood}.webp`, { cacheBuster });\n\n(async () => {\n const status = document.getElementById('status');\n const image = document.getElementById('image');\n const title = document.getElementById('title');\n const spinner = document.getElementById('spinner');\n const anubisVersion = JSON.parse(document.getElementById('anubis_version').textContent);\n\n // const testarea = document.getElementById('testarea');\n\n // const videoWorks = await testVideo(testarea);\n // console.log(`videoWorks: ${videoWorks}`);\n\n // if (!videoWorks) {\n // title.innerHTML = \"Oh no!\";\n // status.innerHTML = \"Checks failed. Please check your browser's settings and try again.\";\n // image.src = imageURL(\"sad\");\n // spinner.innerHTML = \"\";\n // spinner.style.display = \"none\";\n // return;\n // }\n\n status.innerHTML = 'Calculating...';\n\n const { challenge, difficulty } = await fetch(\"/.within.website/x/cmd/anubis/api/make-challenge\", { method: \"POST\" })\n .then(r => {\n if (!r.ok) {\n throw new Error(\"Failed to fetch config\");\n }\n return r.json();\n })\n .catch(err => {\n title.innerHTML = \"Oh no!\";\n status.innerHTML = `Failed to fetch config: ${err.message}`;\n image.src = imageURL(\"sad\");\n spinner.innerHTML = \"\";\n spinner.style.display = \"none\";\n throw err;\n });\n\n status.innerHTML = `Calculating...
Difficulty: ${difficulty}`;\n\n const t0 = Date.now();\n const { hash, nonce } = await process(challenge, difficulty);\n const t1 = Date.now();\n console.log({ hash, nonce });\n\n title.innerHTML = \"Success!\";\n status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`;\n image.src = imageURL(\"happy\", anubisVersion);\n spinner.innerHTML = \"\";\n spinner.style.display = \"none\";\n\n setTimeout(() => {\n const redir = window.location.href;\n window.location.href = u(\"/.within.website/x/cmd/anubis/api/pass-challenge\", { response: hash, nonce, redir, elapsedTime: t1 - t0 });\n }, 250);\n})();"], + "mappings": "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", + "names": ["process", "data", "difficulty", "threads", "resolve", "reject", "webWorkerURL", "processTask", "workers", "i", "worker", "event", "sha256", "text", "encoded", "uint8ArrayToHexString", "arr", "c", "hash", "nonce", "currentHash", "thisHash", "valid", "j", "byteIndex", "nibbleIndex", "u", "url", "params", "result", "kv", "k", "v", "imageURL", "mood", "cacheBuster", "status", "image", "title", "spinner", "anubisVersion", "challenge", "difficulty", "r", "err", "t0", "hash", "nonce", "process", "t1", "redir"] } diff --git a/cmd/anubis/static/js/main.mjs.zst b/cmd/anubis/static/js/main.mjs.zst index 3f27a9ef508bce52fa235d55db9aa10926b1757a..1b713784b8bf098a36a3b6bd82fb5ba450ed7d69 100644 GIT binary patch literal 1191 zcmV;Y1X%khwJ-f-%LmOQ0P0Cd4JzQUHV;q)#-=lsHDjnzv-`<#ob4ZmT0=QnPT-Wr zt3WMDY4V9NMl*(A#Q@9z#sD7lqkUR*pDIO-*_xyR2wDQtZ_>#kgz{2)(QzVV6H?iW zuQXcd)VNFZQ9J941^-=U!`GY$ceo;mdD@aF_uP_a2?NV%Q#MG*djZoQO|8++Mqo3Pf zqi_L(tIM~9?036Hh`NPk(sP+EeS2k5-fVa2P3oYXC(!w6PM;<5gQsgo;Ub$0EtAH7 zQ*tU5ZtVtjB~-SSv`+fiq8nYUEMF&1lLxm)8i~q!SQHWjRs`h0f(YlFvlSH@Y# zRAx%8_w28c!q5J&fRLx9)Xqpd+FB|?v59@>&ww1Yg+O8@Gx}%HZ(relBZk`Qg{V7? z!tM-uV>2yp$B`oKnLh~lqVz1`M&L;9x0b7+lkH+Q3H zO{)g`yl-dX3Teq~DfxMoZ`r8UnqKwHS7#R~B^qw5nzilQ!e?ancx$okTX*J**jn`xRnm3(s@z+PZnC zp)$DfD{k%RKf5{YPouI;D4cP^daAg{lyY<3WO4qO*2M5xg*=rXCgC7>1pp$3TJ3<^ zj10Lb;){MR(Ygd+7y!-MHLVDW2{tVV>>9CbCLtbSJL@`WRNHIiXyO_t_a)j;)cCHD zDM}RaQ@uYfld}p<62$YMRAYJQNgveO?@1YKHIFT3bs?v0?S#(O&kq`oi62xj9#OzC zNv3+zsT_r6R4HE-k*TSkVU z)~UP2g!=Q#gEslFPLwp0WzUf zO>!I|i6~55P!iL|80Q4wTUk+4`6pmDDUsf}HFwxC6cQb;E??TzFC zNrW4SL8$xKFYmgO4W+DdAyWT&D{`_803%f&SKDwsC+TD+ngXn`stxf&P!s`lX5%<< zld3>K91UJ&%jxTdyrgm~N2FfL<@P%YSA%&*uBm@w&FrImGw(MsbL9EP2Q1WD>Dn@^ z#QOoh286+d$50G@)gWNij=Uyq-$MrthPDb2t_6Jl#kU@Kvi;D+TZV#&RMUg1X~M&3 zSbz`W>M$iQA}tR?6F3}SgJ24KvM_>N32F#p3Xx+k@H%4*X(A2)ar!+S^kr>TKZ~^B zz;o`XfQI76dYWs5P(`b46&{IiRjk2k#5y5k_^-8q!jnyIv~b63AS8Z-S^YqDV+OCv FIXQSIK0N>c literal 982 zcmV;{11bC{wJ-f-WCleZ0Cv5vB=C0IFp7Y3-Ewm{8!@BPF<|M@>mNJFLc=8mhq5u2san;#q)m3ZR(VD95+<_6!jO}d z4a0bEitX$<*Huklp*I=3#`ab6H22?6xEFQbN%s4vHJe*9`vbmfty)jjMC|~wv6E=drUnoPor}_IKX9c zqknC##ex}m?AC$;Lcv1?$71h$Vd$;&BN%@HnV!+Nl71S%EUeY);K_x65X%siV(zD)s7s;=ZMl5)N(NXc_)7D%mnevsMpIw{SU0aS> z46@KbtTgd^c`tXu#D<`3j6EV$B{u_<%fyt(ukRG+5sXv{G)2k@WuJXyGf#5#oOu)x z2Pom>*tM#?Yxe8UC!MMh#i{F$pM@ds8e~}%bz-*VHD|s&KLC_Y&q<~yStodrOi_xl zabnsX0SX1+X{=Q(VG&%_D$qKm(mJh7hIYDj*_7{eR|-B**<*Sq?)Y^LMQP-7pPec? zpQ>2^m2`JF0V`X=ePhIMJdl$j#x<5{`Sw);PDiAA2?xp?X%MN@t(7eo)Tl(c=KZZ( zkosa1jMJYxk?QGYz^RD170}X&GB}E_u`$_B@^oF;B0D<$4zv<0#bUGPoCs(rFhXMx zGxd_VaFm9yQjk#~Nhl~b?Ti7~NCKL|T>-;hD0TP= zBhB?8oNiotDO81dr|ec_`gGuCMS}ayM`tbTm_h^uF>;7=-~-F>FI`-O9S1^42S?Fg zhvR<%cp6%fhNJ{gvpw|7j^ZJ{vWr>$66L`K!@bQk_C`&-PZ