فهرست منبع

Show a progress bar for the probability of completing the proof of work challenge (#87)

Since the challenge is done off of the main thread, there is no simple
way to report the progress done towards completing it. This change
adds a callback parameter, `progressCallback`, which is called with
the most recently attempted nonce every ~1024 iterations (should this
be configurable?). For the single-threaded "slow" algorithm, this is
exactly every 1024 iterations. For the multi-threaded "fast" algorithm,
threads take turns reporting in a round-robin as then notice they
have passed a multiple of 1024. This complexity is to avoid individual
threads falling behind their siblings due to the overhead of messaging
the main thread. To minimize this overhead as much as possible, a
regular number is sent instead of an object.

With the new information provided by the callback, a hash rate display
is added to the challenge page. This display is updated at most once
per second and set with tabular numbers to avoid the constantly changing
value being too visually distracting.

* web: show a progress bar based on completion probability

To provide more feedback to the user, the spinner is replaced with a
progress bar of the probability the challenge is complete. Since it
looks a little weird that a progress bar would fill up a quarter of the
way and then jump to the end (even though the probability would make
that happen 1 in 4 times), the bar is mapped with a quadratic easing
function to move faster at the beginning and then slow down as the
probability of redirection increases. If the probability exceeds 90%,
a message appears letting the user know things are taking longer than
expected and to continue being patient.

Signed-off-by: Xe Iaso <me@xeiaso.net>
jae beller 2 ماه پیش
والد
کامیت
3771a3b627
6فایلهای تغییر یافته به همراه111 افزوده شده و 136 حذف شده
  1. 1 0
      docs/docs/CHANGELOG.md
  2. 20 119
      web/index.templ
  3. 0 0
      web/index_templ.go
  4. 47 10
      web/js/main.mjs
  5. 15 3
      web/js/proof-of-work-slow.mjs
  6. 28 4
      web/js/proof-of-work.mjs

+ 1 - 0
docs/docs/CHANGELOG.md

@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - The Dockerfile has been removed as it is no longer in use
 - Developer documentation has been added to the docs site
 - Show more errors when some predictable challenge page errors happen ([#150](https://github.com/TecharoHQ/anubis/issues/150))
+- Verification page now shows hash rate and a progress bar for completion probability.
 
 ## v1.15.0
 

+ 20 - 119
web/index.templ

@@ -27,120 +27,28 @@ templ base(title string, body templ.Component) {
       text-align: center;
     }
 
-    .lds-roller,
-    .lds-roller div,
-    .lds-roller div:after {
-      box-sizing: border-box;
+    #status {
+      font-variant-numeric: tabular-nums;
     }
 
-    .lds-roller {
-      display: inline-block;
-      position: relative;
-      width: 80px;
-      height: 80px;
+    #progress {
+      display: none;
+      width: min(20rem, 90%);
+      height: 2rem;
+      border-radius: 1rem;
+      overflow: hidden;
+      margin: 1rem 0 2rem;
+      outline-color: #b16286;
+      outline-offset: 2px;
+      outline-style: solid;
+      outline-width: 4px;
     }
 
-    .lds-roller div {
-      animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
-      transform-origin: 40px 40px;
-    }
-
-    .lds-roller div:after {
-      content: " ";
-      display: block;
-      position: absolute;
-      width: 7.2px;
-      height: 7.2px;
-      border-radius: 50%;
-      background: currentColor;
-      margin: -3.6px 0 0 -3.6px;
-    }
-
-    .lds-roller div:nth-child(1) {
-      animation-delay: -0.036s;
-    }
-
-    .lds-roller div:nth-child(1):after {
-      top: 62.62742px;
-      left: 62.62742px;
-    }
-
-    .lds-roller div:nth-child(2) {
-      animation-delay: -0.072s;
-    }
-
-    .lds-roller div:nth-child(2):after {
-      top: 67.71281px;
-      left: 56px;
-    }
-
-    .lds-roller div:nth-child(3) {
-      animation-delay: -0.108s;
-    }
-
-    .lds-roller div:nth-child(3):after {
-      top: 70.90963px;
-      left: 48.28221px;
-    }
-
-    .lds-roller div:nth-child(4) {
-      animation-delay: -0.144s;
-    }
-
-    .lds-roller div:nth-child(4):after {
-      top: 72px;
-      left: 40px;
-    }
-
-    .lds-roller div:nth-child(5) {
-      animation-delay: -0.18s;
-    }
-
-    .lds-roller div:nth-child(5):after {
-      top: 70.90963px;
-      left: 31.71779px;
-    }
-
-    .lds-roller div:nth-child(6) {
-      animation-delay: -0.216s;
-    }
-
-    .lds-roller div:nth-child(6):after {
-      top: 67.71281px;
-      left: 24px;
-    }
-
-    .lds-roller div:nth-child(7) {
-      animation-delay: -0.252s;
-    }
-
-    .lds-roller div:nth-child(7):after {
-      top: 62.62742px;
-      left: 17.37258px;
-    }
-
-    .lds-roller div:nth-child(8) {
-      animation-delay: -0.288s;
-    }
-
-    .lds-roller div:nth-child(8):after {
-      top: 56px;
-      left: 12.28719px;
-    }
-
-    .mx-auto {
-      margin-left: auto;
-      margin-right: auto;
-    }
-
-    @keyframes lds-roller {
-      0% {
-        transform: rotate(0deg);
-      }
-
-      100% {
-        transform: rotate(360deg);
-      }
+    .bar-inner {
+      background-color: #b16286;
+      height: 100%;
+      width: 0;
+      transition: width 0.25s ease-in;
     }
       </style>
 			@templ.JSONScript("anubis_version", anubis.Version)
@@ -181,15 +89,8 @@ templ index() {
 		/>
 		<p id="status">Loading...</p>
 		<script async type="module" src={ "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script>
-		<div id="spinner" class="lds-roller mx-auto" style="display:none;">
-			<div></div>
-			<div></div>
-			<div></div>
-			<div></div>
-			<div></div>
-			<div></div>
-			<div></div>
-			<div></div>
+		<div id="progress" role="progressbar" aria-labelledby="status">
+			<div class="bar-inner"></div>
 		</div>
 		<details>
 			<summary>Why am I seeing this?</summary>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
web/index_templ.go


+ 47 - 10
web/js/main.mjs

@@ -37,7 +37,7 @@ const dependencies = [
   const status = document.getElementById('status');
   const image = document.getElementById('image');
   const title = document.getElementById('title');
-  const spinner = document.getElementById('spinner');
+  const progress = document.getElementById('progress');
   const anubisVersion = JSON.parse(document.getElementById('anubis_version').textContent);
 
   const ohNoes = ({
@@ -46,8 +46,7 @@ const dependencies = [
     title.innerHTML = titleMsg;
     status.innerHTML = statusMsg;
     image.src = imageSrc;
-    spinner.innerHTML = "";
-    spinner.style.display = "none";
+    progress.style.display = "none";
   };
 
   if (!window.isSecureContext) {
@@ -68,8 +67,7 @@ const dependencies = [
   //   title.innerHTML = "Oh no!";
   //   status.innerHTML = "Checks failed. Please check your browser's settings and try again.";
   //   image.src = imageURL("sad");
-  //   spinner.innerHTML = "";
-  //   spinner.style.display = "none";
+  //   progress.style.display = "none";
   //   return;
   // }
 
@@ -112,20 +110,59 @@ const dependencies = [
     return;
   }
 
-  status.innerHTML = `Calculating...<br/>Difficulty: ${rules.report_as}`;
-  spinner.style.display = "block";
+  status.innerHTML = `Calculating...<br/>Difficulty: ${rules.report_as}, `;
+  progress.style.display = "inline-block";
 
+  // the whole text, including "Speed:", as a single node, because some browsers
+  // (Firefox mobile) present screen readers with each node as a separate piece
+  // of text.
+  const rateText = document.createTextNode("Speed: 0kH/s");
+  status.appendChild(rateText);
+
+  let lastSpeedUpdate = 0;
+  let showingApology = false;
+  const likelihood = Math.pow(16, -rules.report_as);
   try {
     const t0 = Date.now();
-    const { hash, nonce } = await process(challenge, rules.difficulty);
+    const { hash, nonce } = await process(
+      challenge,
+      rules.difficulty,
+      (iters) => {
+        const delta = Date.now() - t0;
+        // only update the speed every second so it's less visually distracting
+        if (delta - lastSpeedUpdate > 1000) {
+          lastSpeedUpdate = delta;
+          rateText.data = `Speed: ${(iters / delta).toFixed(3)}kH/s`;
+        }
+
+        // the probability of still being on the page is (1 - likelihood) ^ iters.
+        // by definition, half of the time the progress bar only gets to half, so
+        // apply a polynomial ease-out function to move faster in the beginning
+        // and then slow down as things get increasingly unlikely. quadratic felt
+        // the best in testing, but this may need adjustment in the future.
+        const probability = Math.pow(1 - likelihood, iters);
+        const distance = (1 - Math.pow(probability, 2)) * 100;
+        progress["aria-valuenow"] = distance;
+        progress.firstElementChild.style.width = `${distance}%`;
+
+        if (probability < 0.1 && !showingApology) {
+          status.append(
+            document.createElement("br"),
+            document.createTextNode(
+              "Verification is taking longer than expected. Please do not refresh the page.",
+            ),
+          );
+          showingApology = true;
+        }
+      },
+    );
     const t1 = Date.now();
     console.log({ hash, nonce });
 
     title.innerHTML = "Success!";
     status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`;
     image.src = imageURL("happy", anubisVersion);
-    spinner.innerHTML = "";
-    spinner.style.display = "none";
+    progress.style.display = "none";
 
     setTimeout(() => {
       const redir = window.location.href;

+ 15 - 3
web/js/proof-of-work-slow.mjs

@@ -1,6 +1,11 @@
 // https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm
 
-export default function process(data, difficulty = 5, _threads = 1) {
+export default function process(
+  data,
+  difficulty = 5,
+  progressCallback = null,
+  _threads = 1,
+) {
   console.debug("slow algo");
   return new Promise((resolve, reject) => {
     let webWorkerURL = URL.createObjectURL(new Blob([
@@ -10,8 +15,12 @@ export default function process(data, difficulty = 5, _threads = 1) {
     let worker = new Worker(webWorkerURL);
 
     worker.onmessage = (event) => {
-      worker.terminate();
-      resolve(event.data);
+      if (typeof event.data === "number") {
+        progressCallback?.(event.data);
+      } else {
+        worker.terminate();
+        resolve(event.data);
+      }
     };
 
     worker.onerror = (event) => {
@@ -47,6 +56,9 @@ function processTask() {
       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'));
 

+ 28 - 4
web/js/proof-of-work.mjs

@@ -1,4 +1,9 @@
-export default function process(data, difficulty = 5, threads = (navigator.hardwareConcurrency || 1)) {
+export default function process(
+  data,
+  difficulty = 5,
+  progressCallback = null,
+  threads = (navigator.hardwareConcurrency || 1),
+) {
   console.debug("fast algo");
   return new Promise((resolve, reject) => {
     let webWorkerURL = URL.createObjectURL(new Blob([
@@ -11,9 +16,12 @@ export default function process(data, difficulty = 5, threads = (navigator.hardw
       let worker = new Worker(webWorkerURL);
 
       worker.onmessage = (event) => {
-        workers.forEach(worker => worker.terminate());
-        worker.terminate();
-        resolve(event.data);
+        if (typeof event.data === "number") {
+          progressCallback?.(event.data);
+        } else {
+          workers.forEach(worker => worker.terminate());
+          resolve(event.data);
+        }
       };
 
       worker.onerror = (event) => {
@@ -55,6 +63,8 @@ function processTask() {
       let nonce = event.data.nonce;
       let threads = event.data.threads;
 
+      const threadId = nonce;
+
       while (true) {
         const currentHash = await sha256(data + nonce);
         const thisHash = new Uint8Array(currentHash);
@@ -78,7 +88,21 @@ function processTask() {
           break;
         }
 
+        const oldNonce = nonce;
         nonce += threads;
+
+        // send a progess update every 1024 iterations. since each thread checks
+        // separate values, one simple way to do this is by bit masking the
+        // nonce for multiples of 1024. unfortunately, if the number of threads
+        // is not prime, only some of the threads will be sending the status
+        // update and they will get behind the others. this is slightly more
+        // complicated but ensures an even distribution between threads.
+        if (
+          nonce > oldNonce | 1023 && // we've wrapped past 1024
+          (nonce >> 10) % threads === threadId // and it's our turn
+        ) {
+          postMessage(nonce);
+        }
       }
 
       postMessage({

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است