1// Page wiring for the kangaroo demo. 2// 3// The whole page exists to make one claim runnable instead of asserted: a 4// public key from a narrow, known range is recoverable in about the square root 5// of the work a brute-force sweep needs. Everything on screen is generated in 6// front of the visitor so the recovery can be checked rather than believed. 7 8import { expectedWork, medianBruteKeys } from "./kangaroo.js"; 9import { scalarMul, compressPubkey } from "../reference/secp.js"; 10import { privToAddress } from "../reference/address.js"; 11 12const $ = (id) => document.getElementById(id); 13 14// The reference GPU for the brute-force column. Same card and same measured 15// figure the main app's odds ladder uses, so the two pages cannot disagree. 16const REF_GPU = "RTX 3070"; 17const REF_RATE = 22_550_000; 18 19// Every range runs on the GPU, both sides. 20// 21// There used to be JavaScript ranges below 2^44. They were dropped because they 22// made the page tell two different stories without explaining the join. A 23// JavaScript brute force manages about 450 keys/sec against 5.99 M on the same 24// laptop's GPU, so the page showed rates differing by four orders of magnitude 25// with no account of why, and the algorithmic saving looked roughly a hundred 26// times larger in JavaScript than it is. One engine means one comparison, and 27// every number on the page is then measured the same way. 28// 29// The cost is that a visitor without WebGPU cannot run anything. They still get 30// the whole argument in prose, and the main app already refuses them, so the 31// page is consistent with the rest of the project rather than uniquely broken. 32// 33// Times are from an M1 Air at 4.8 to 5.5M jumps/sec, which is the slow end. A 34// faster card cuts them roughly in proportion to its search rate. 35const RANGES = [ 36 { bits: 44n, label: "2^44", note: "a few seconds" }, 37 { bits: 52n, label: "2^52", note: "about 30 seconds" }, 38 { bits: 60n, label: "2^60", note: "about 10 minutes" }, 39]; 40 41const gpuState = { checked: false, device: null, label: "gpu", error: null }; 42 43const ensureGpu = async () => { 44 if (gpuState.checked) return gpuState.device; 45 gpuState.checked = true; 46 try { 47 const { initWebGPU, browserName } = await import("../webgpu.js"); 48 const { adapter, device } = await initWebGPU(); 49 gpuState.device = device; 50 // Name the silicon. Any jump rate quoted from this page is worthless 51 // without knowing what produced it, which is the same reason the main app 52 // shows a GPU ADAPTER tile. Kept in step with it on purpose: this page is about to be 53 // announced, so its readings arrive as community reports carrying the same ambiguities, 54 // and the same two fixes apply. 55 // 56 // info.description first, because Chromium on Windows puts the full card name there 57 // ("NVIDIA GeForce RTX 4090") while vendor and architecture give only the family. 58 // Deduplicated after, because Safari reports both as "apple" and doubling it reads like 59 // a bug. Firefox leaves both empty, which is what the fallback is for. 60 // 61 // The browser is named because it matters as much as the silicon here: an M3 Pro 62 // measured 14.94 MK/s on Chrome and 7.72 on Firefox, and Firefox is the one engine that 63 // leaves adapter.info empty, so the tile used to read just "gpu (wgsl)" on exactly the 64 // browser where knowing mattered most. 65 const info = adapter.info || {}; 66 const gpuPart = info.description ||
67 [...new Set([info.vendor, info.architecture].filter(Boolean))].join(" · "); 68 gpuState.label = [gpuPart || "gpu (wgsl)", browserName()].filter(Boolean).join(" · "); 69 } catch (err) { 70 gpuState.error = err; 71 gpuState.device = null; 72 } 73 return gpuState.device; 74}; 75 76const SECONDS = { minute: 60, hour: 3600, day: 86400, year: 31556952 }; 77 78const prettyDuration = (sec) => { 79 if (!Number.isFinite(sec)) return "longer than anyone has measured"; 80 if (sec < 1) return "under a second"; 81 if (sec < SECONDS.minute) return `${sec.toFixed(sec < 10 ? 1 : 0)} seconds`; 82 if (sec < SECONDS.hour) return `${(sec / SECONDS.minute).toFixed(1)} minutes`; 83 if (sec < SECONDS.day) return `${(sec / SECONDS.hour).toFixed(1)} hours`; 84 if (sec < SECONDS.year) return `${(sec / SECONDS.day).toFixed(1)} days`; 85 const years = sec / SECONDS.year; 86 // Up to 10,000 rather than 1,000: the 2^60 brute-force figure moved from 810 to 1,123 years when 87 // the median was corrected, and at the old threshold that rendered as "1 thousand years", which 88 // is both vaguer than the number it replaced and disagrees with the social card. 89 if (years < 10000) return `${Math.round(years).toLocaleString("en-US")} years`; 90 if (years < 1e6) return `${(years / 1000).toFixed(0)} thousand years`; 91 if (years < 1e9) return `${(years / 1e6).toFixed(1)} million years`; 92 return `${(years / 1e9).toFixed(1)} billion years`; 93}; 94 95// BigInt-aware. width*ln(2) for the larger ranges exceeds 2^53, where Number() would print a 96// fake round tail (...166,000 instead of ...166,001) on a page whose entire argument is that the 97// numbers are checkable. BigInt has its own toLocaleString. 98const fmt = (n) => (typeof n === "bigint" ? n : Number(n)).toLocaleString("en-US"); 99 100// One formatter for measured rates, used by the card, the progress line and the 101// result note. Two of these printed the same measurement as "5.99 M" and 102// "6.0 M" on one screen, which invites a reader to wonder which is real on a 103// page whose whole argument is that the numbers are checkable. 104const fmtRate = (r) => (r >= 1e6 ? (r / 1e6).toFixed(2) + " M" : fmt(Math.round(r))); 105const hex = (n, len = 64) => n.toString(16).padStart(len, "0"); 106 107// A 64-character key is mostly leading zeros, and the page asks the reader to 108// check the recovered value against the generated one. The eye cannot compare 109// two 64-char strings, so the padding is dimmed and the significant tail left at 110// full strength. Writes markup, so it takes only hex from a BigInt. 111const hexSplit = (n, len = 64) => { 112 if (n === null || n === undefined) return ""; 113 const s = hex(n, len); 114 const tail = s.replace(/^0+/, "") || "0"; 115 const pad = s.slice(0, s.length - tail.length); 116 return (pad ? `<span class="hex-pad">${pad}</span>` : "") + tail; 117}; 118const toHexBytes = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); 119 120/** A key drawn with the Web Crypto CSPRNG, never Math.random. */ 121const randomKeyInRange = (start, width) => { 122 const bytes = new Uint8Array(40); 123 crypto.getRandomValues(bytes); 124 let v = 0n; 125 for (const b of bytes) v = (v << 8n) | BigInt(b); 126 return start + (v % width); 127}; 128 129const state = { 130 // 2^52: about half a minute of kangaroo against roughly three years of brute 131 // force. Long enough to feel real, short enough that people wait for it. 132 range: RANGES[1], 133 key: null, 134 pub: null, 135 running: false, 136 cancelled: false, 137 benching: false, 138 benchFailed: false, 139 // navigator.gpu existing is not the same as getting a device, so this is 140 // only known once something has actually asked for one. 141 noGpu: false, 142}; 143 144// --- rendering ------------------------------------------------------------ 145 146// Focus goes to whatever now occupies the position of the control that was just 147// pressed, so the viewport never moves. Two earlier attempts moved it instead: 148// the Run button in step 04 threw the page down past two sections, and step 02 149// is tall enough that scrolling all of it into view pins it to the bottom edge 150// and scrolls the hero off. Generate is replaced in place by "Generate a 151// different one", and Run by "Stop", so both are already under the cursor. 152// The news that a key exists is carried by the status announcement instead. 153const PHASE_FOCUS = { ready: "regenerate", running: "cancel-kangaroo" }; 154 155const setPhase = (name) => { 156 const prev = document.activeElement; 157 document.body.dataset.phase = name; 158 for (const el of document.querySelectorAll("[data-phase-show]")) { 159 el.hidden = !el.dataset.phaseShow.split(" ").includes(name); 160 } 161 // Two columns can otherwise disagree about whether the page is busy: each 162 // handler guards itself, but silently, so the other control just looked live. 163 $("run-bench").disabled = state.running; 164 for (const el of document.querySelectorAll("#range-choices input")) { 165 el.disabled = !navigator.gpu || state.running || state.benching; 166 } 167 // Every transition on this page is triggered by clicking a button that then 168 // hides itself, which drops focus to <body> and sends a keyboard user back to 169 // the top of the document. Hand focus to whatever replaced it. On `done` that 170 // is the result section, and focusing freshly revealed content is also what 171 // makes a screen reader read the verdict, which a live region written while 172 // its ancestor was hidden would not. 173 if (!prev || prev === document.body || prev.offsetParent !== null) return; 174 const next = name === "done" ? document.querySelector(".kg-result") : $(PHASE_FOCUS[name]); 175 if (next && !next.hidden) next.focus(); 176}; 177 178/** Return the target block to its empty state. */ 179const resetTarget = () => { 180 state.key = null; 181 state.pub = null; 182 for (const id of ["target-privkey", "target-address", "target-pubkey"]) { 183 const el = $(id);
184 el.textContent = "generated when you press the button"; 185 el.classList.add("empty"); 186 } 187 setPhase("idle"); 188}; 189 190const renderRangeChoices = () => { 191 const hasGpu = !!navigator.gpu; 192 const wrap = $("range-choices"); 193 wrap.innerHTML = ""; 194 for (const r of RANGES) { 195 const blocked = !hasGpu; 196 const id = `range-${r.bits}`; 197 const label = document.createElement("label"); 198 label.className = "range-choice" + (blocked ? " blocked" : ""); 199 label.innerHTML = 200 `<input type="radio" name="range" id="${id}" value="${r.bits}"${r === state.range ? " checked" : ""}${blocked ? " disabled" : ""}>` + 201 `<span class="rc-label">${r.label}</span>` + 202 `<span class="rc-note">${blocked ? "no WebGPU here" : r.note}</span>`; 203 label.querySelector("input").addEventListener("change", () => { 204 // Never mid-flight. Resetting here would hide Stop while the GPU kept 205 // walking, and the completed result would then be thrown away. 206 if (state.running || state.benching) { renderRangeChoices(); return; } 207 state.range = r; 208 // The key belongs to the old interval, and renderProjection is about to 209 // move rangeStart. Keeping it would show a key outside the range printed 210 // directly above it, and Run would then walk an interval it cannot be in. 211 if (state.key !== null) resetTarget(); 212 renderProjection(); 213 }); 214 wrap.appendChild(label); 215 } 216 $("gpu-hint").hidden = hasGpu; 217}; 218 219// Measured brute-force rates, cached per engine for this page load. 220// 221// Keyed by engine rather than by range, so switching 2^44 to 2^52 recomputes 222// only the projection. Deliberately not persisted: a rate measured while the 223// machine was busy would be wrong for every later visit, and it is cheap to 224// take again. 225let benchResult = null; 226 227const renderProjection = () => { 228 const width = 2n ** state.range.bits; 229 const { bruteForceKeys, kangarooJumps } = expectedWork(width); 230 231 // A non-zero start, so the range reads as an interval rather than as "small 232 // numbers", which is the thing people wrongly assume makes this work. 233 state.rangeStart = 2n ** (state.range.bits + 8n); 234 $("target-range-lo").textContent = state.rangeStart.toString(16); 235 $("target-range-hi").textContent = (state.rangeStart + width).toString(16); 236 237 $("proj-range").textContent = state.range.label; 238 239 // The herds figure names the range too, so it follows the picker rather than 240 // stating the default while the reader looks at a different interval. SVG has 241 // no <sup>, so the exponent is superscript digits. 242 $("fig-range-label").textContent = 243 "2" + [...state.range.bits.toString()].map((d) => "â°Â¹Â²Â³â´âµâ¶â·â¸â¹"[+d]).join(""); 244 $("proj-brute-keys").textContent = fmt(bruteForceKeys); 245 $("proj-brute-keys-2").textContent = fmt(bruteForceKeys); 246 $("proj-brute-time").textContent = prettyDuration(Number(bruteForceKeys) / REF_RATE); 247 $("proj-kangaroo-jumps").textContent = fmt(kangarooJumps); 248 249 // Work over work. No hardware in it, so it holds on any machine in any 250 // browser, and it rises monotonically across every range. Deriving it from 251 // measured times instead would swing it by 150x at the JavaScript-to-GPU 252 // boundary and overstate the algorithm's saving in JavaScript by more than a 253 // hundredfold, because JavaScript brute force is anomalously slow rather than 254 // the GPU being anomalously fast. 255 $("proj-ratio").textContent = fmt(bruteForceKeys / (kangarooJumps || 1n)); 256 257 renderBench(); 258}; 259 260/** Show whichever of the four benchmark states currently applies. */ 261const renderBench = () => { 262 const bench = benchResult; 263 const measured = !!bench && !state.benchFailed; 264 265 $("bench-start").hidden = measured || state.benching || state.noGpu; 266 $("bench-running").hidden = !state.benching; 267 $("bench-measured").hidden = !measured; 268 $("measured-note").hidden = !measured; 269 $("bench-fallback").hidden = measured; 270 $("fallback-note").hidden = measured; 271 $("bench-invite").hidden = measured || state.noGpu; 272 273 // Both routes to "no device" land here, so this is the one place that has to 274 // reveal the standing note beside the controls. Measuring alone never reaches 275 // the result panel, and without this that route explains nothing at the top. 276 if (state.noGpu) $("gpu-hint").hidden = false;
277 278 if (!measured) return; 279 280 const width = 2n ** state.range.bits; 281 const { bruteForceKeys } = expectedWork(width); 282 const seconds = Number(bruteForceKeys) / bench.rate; 283 284 $("measured-title").textContent = "Your GPU, just measured"; 285 $("measured-rate").textContent = fmtRate(bench.rate); 286 $("measured-engine").textContent = bench.label; 287 $("measured-range").textContent = state.range.label; 288 $("measured-time").textContent = prettyDuration(seconds); 289 $("measured-keys").textContent = fmt(bruteForceKeys); 290 291 $("measured-note").textContent = 292 "Same machine, both sides. The kangaroo runs on this GPU too, so the " + 293 "difference you are about to see is the algorithm and nothing else."; 294}; 295 296const renderTarget = async () => { 297 for (const id of ["target-privkey", "target-address", "target-pubkey"]) { 298 $(id).classList.remove("empty"); 299 } 300 $("target-privkey").innerHTML = hexSplit(state.key); 301 $("target-pubkey").textContent = toHexBytes(compressPubkey(state.pub)); 302 $("target-address").textContent = "deriving..."; 303 $("target-address").textContent = await privToAddress(state.key); 304}; 305 306const renderLive = ({ jumps, dps, elapsedMs }) => { 307 $("live-engine").textContent = gpuState.label; 308 $("live-jumps").textContent = fmt(jumps); 309 $("live-dps").textContent = fmt(dps); 310 $("live-elapsed").textContent = (elapsedMs / 1000).toFixed(1) + "s"; 311 const rate = elapsedMs > 0 ? jumps / (elapsedMs / 1000) : 0; 312 $("live-rate").textContent = fmt(Math.round(rate)); 313}; 314 315/** 316 * Fill the fix block with platform-specific instructions for getting WebGPU 317 * working, reusing the same helper the main page shows in #gpu-info. 318 * 319 * Worth the round trip because the most common real failure is not an incapable 320 * machine, it is Linux plus a Chromium browser with Vulkan switched off, and 321 * that is two flags away from working. Telling someone it does not work without 322 * telling them that is a diagnosis with no remedy. 323 */ 324const showGpuFix = async () => { 325 const el = $("result-gpu-fix"); 326 try { 327 const { describeWebGPUFailure } = await import("../webgpu.js"); 328 const why = (gpuState.error && gpuState.error.message) || "no WebGPU device"; 329 // Drop the leading FAILED: line. The verdict directly above already said it, 330 // and repeating it here just pushes the actual steps further down. 331 el.textContent = describeWebGPUFailure(why).replace(/^FAILED:[^\n]*\n+/, ""); 332 el.hidden = false; 333 } catch (err) { 334 // The module that explains the failure failing is not worth a second banner. 335 console.warn("could not describe the WebGPU failure:", err); 336 el.hidden = true; 337 } 338}; 339 340const renderResult = (res) => { 341 const matched = res.key !== null && res.key === state.key; 342 343 // Reset before branching. Two of the branches below return early, and without 344 // this a failure inherits the previous run's jump count and timing. 345 for (const id of ["result-jumps", "result-elapsed", "result-vs"]) $(id).textContent = ""; 346 $("result-brute-dt").hidden = true; 347 $("result-brute").hidden = true; 348 $("result-facts").hidden = false; 349 $("result-gpu-fix").hidden = true; 350 $("result-key").className = res.key === null ? "mono" : "mono accent"; 351 if (res.key === null) $("result-key").textContent = "not recovered"; 352 else $("result-key").innerHTML = hexSplit(res.key); 353 $("result-original").innerHTML = hexSplit(state.key); 354 const verdict = $("result-verdict"); 355 if (res.noGpu) { 356 verdict.textContent = 357 "This demo needs WebGPU and this browser did not provide a device. " + 358 "Everything above still describes what the attack does; it just cannot run here."; 359 verdict.className = "verdict bad"; 360 // Nothing ran, so "not recovered", a blank jump count and a blank time are 361 // all noise dressed up as data. The verdict is the entire message. 362 $("result-facts").hidden = true; 363 state.noGpu = true; 364 showGpuFix(); 365 renderBench(); 366 return; 367 } 368 if (res.gpuError) { 369 verdict.textContent = 370 "The GPU walk failed to start: " + (res.gpuError.message || res.gpuError) + 371 ". The details are in the browser console."; 372 verdict.className = "verdict bad"; 373 return; 374 } 375 if (matched) { 376 verdict.textContent = "Recovered. It matches the key generated above."; 377 verdict.className = "verdict ok"; 378 } else if (res.stopped) { 379 verdict.textContent = "Stopped before the walk finished."; 380 verdict.className = "verdict"; 381 } else { 382 verdict.textContent = "The walk ran out of budget without a collision. Try again or pick a smaller range."; 383 verdict.className = "verdict bad"; 384 } 385 $("result-jumps").textContent = fmt(res.jumps); 386 $("result-elapsed").textContent = (res.elapsedMs / 1000).toFixed(2) + "s"; 387 388 // If the machine was measured, both figures in this table come from it, which 389 // is the sentence the page has been building toward. If it was not, say whose 390 // hardware the other number belongs to rather than letting it pass as yours. 391 const width = 2n ** state.range.bits; 392 const bench = benchResult; 393 const measured = bench && !state.benchFailed; 394 const bruteSeconds = Number(medianBruteKeys(width)) / (measured ? bench.rate : REF_RATE); 395 396 $("result-brute-dt").hidden = !measured; 397 $("result-brute").hidden = !measured; 398 if (measured) { 399 $("result-brute-dt").textContent = "Brute force, same GPU"; 400 $("result-brute").textContent = prettyDuration(bruteSeconds); 401 $("result-vs").textContent = 402 `Both figures come from this machine. A sweep of the same range needs a median ` + 403 `${fmt(medianBruteKeys(width))} keys at your measured ${fmtRate(bench.rate)} keys/sec.`; 404 } else if (!res.stopped) { 405 $("result-vs").textContent = 406 `A brute-force sweep of the same range needs a median ${fmt(medianBruteKeys(width))} keys, ` + 407 `about ${prettyDuration(bruteSeconds)} on an ${REF_GPU}. That is a reference card, ` + 408 `not the machine that just ran the kangaroo.`; 409 } 410}; 411 412// --- running -------------------------------------------------------------- 413 414/** 415 * Measure brute force on the same GPU that is about to run the kangaroo. 416 * 417 * Both sides on one engine is the whole point. The page used to quote a 418 * reference card against the visitor's own walk and the two numbers disagreed 419 * by four orders of magnitude. 420 */ 421const runBench = async () => { 422 if (state.benching || state.running) return; 423 if (benchResult) return; 424 425 state.benching = true; 426 state.benchFailed = false;
427 $("bench-live").textContent = "measuring..."; 428 $("bench-detail").textContent = "Calibrating dispatch size, then timing three of them."; 429 renderBench(); 430 431 try { 432 const device = await ensureGpu(); 433 if (!device) throw new Error("no WebGPU device"); 434 const { benchGpu } = await import("./bench.js"); 435 benchResult = await benchGpu({ 436 label: gpuState.label, 437 onProgress: ({ done, total, rate }) => { 438 $("bench-live").textContent = `${fmtRate(rate)} keys/sec`; 439 $("bench-detail").textContent = `Timed dispatch ${done} of ${total}.`; 440 }, 441 }); 442 } catch (err) { 443 console.warn("benchmark failed, keeping the reference figure:", err); 444 state.benchFailed = true; 445 // Two very different failures land here. With no device at all the kangaroo 446 // cannot run either, so promising that it still will would contradict the 447 // result panel, which says the demo cannot run here. Retrying is pointless 448 // in that case too, so the invitation goes away rather than baiting a click. 449 state.noGpu = !gpuState.device; 450 $("fallback-note").textContent = state.noGpu 451 ? "Your machine could not be measured, because this browser did not provide a WebGPU " + 452 "device. Neither side of this comparison can run here, so both figures are calculated " + 453 "rather than measured on your hardware." 454 : "Your machine could not be measured, so this is a reference card rather than yours, " + 455 "and it is calculated rather than run. The kangaroo still runs on your hardware, " + 456 "so treat the comparison as indicative."; 457 if (!state.noGpu) $("run-bench").textContent = "Try measuring again"; 458 } finally { 459 state.benching = false; 460 renderBench(); 461 } 462}; 463 464const runOnGpu = async (opts, onProgress, device) => { 465 const { runGpu } = await import("./gpu.js"); 466 return runGpu({ 467 ...opts, 468 device, 469 onProgress, 470 shouldStop: () => state.cancelled, 471 onSeed: ({ label, done, total }) => { 472 // Not the ELAPSED tile: a label that says ELAPSED should not read 473 // "seeding tame 40%". RUNNING ON is idle at this point and already has 474 // the small mono treatment for strings. 475 $("live-engine").textContent = `seeding ${label} ${Math.round((done / total) * 100)}%`; 476 }, 477 }); 478}; 479 480const startWalk = async () => { 481 // Both would dispatch onto the same device and corrupt each other's timing, 482 // which is precisely the pair of numbers this page exists to compare. 483 if (state.running || state.benching) return; 484 state.running = true; 485 state.cancelled = false; 486 setPhase("running"); 487 $("live-status").textContent = "Kangaroo walk started. Progress is shown in the counters in the demonstration panel."; 488 renderLive({ jumps: 0, dps: 0, elapsedMs: 0 }); 489 490 const width = 2n ** state.range.bits; 491 $("live-engine").textContent = "detecting..."; 492 const device = await ensureGpu(); 493 494 // No GPU is a refusal, not a fallback. Every range here needs one, and a 495 // JavaScript walk at 2^44 would run for days. 496 if (!device) { 497 state.running = false; 498 renderResult({ key: null, jumps: 0, dps: 0, elapsedMs: 0, stopped: true, noGpu: true }); 499 setPhase("done"); 500 return; 501 } 502 503 let res; 504 try { 505 $("live-engine").textContent = gpuState.label; 506 res = await runOnGpu( 507 { pub: state.pub, rangeStart: state.rangeStart, rangeWidth: width }, 508 renderLive, 509 device 510 ); 511 } catch (err) { 512 // There is nothing to fall back to. Dropping to a JavaScript walk here would 513 // quietly start something that finishes in days, which is worse than saying 514 // the GPU path failed. 515 console.error("GPU walk failed:", err); 516 state.running = false; 517 renderResult({ key: null, jumps: 0, dps: 0, elapsedMs: 0, stopped: true, gpuError: err }); 518 setPhase("done"); 519 return; 520 } 521 522 state.running = false; 523 $("cancel-kangaroo").removeAttribute("aria-disabled"); 524 $("cancel-kangaroo").textContent = "Stop"; 525 renderResult(res); 526 $("live-status").textContent = $("result-verdict").textContent; 527 setPhase("done"); 528}; 529 530const cancelWalk = () => { 531 if (state.cancelled) return; 532 state.cancelled = true; 533 // shouldStop is only read between dispatches, and seeding runs before the 534 // first one, so a press can sit unacknowledged for a while. Say so rather 535 // than looking broken. 536 // aria-disabled, not disabled: disabling the element that currently has focus 537 // drops focus to <body>, which makes setPhase("done") short-circuit and the 538 // result is then neither focused nor announced. 539 const btn = $("cancel-kangaroo"); 540 btn.setAttribute("aria-disabled", "true"); 541 btn.textContent = "Stopping..."; 542 $("live-status").textContent = "Stopping after the current step."; 543}; 544 545const generate = async () => { 546 const width = 2n ** state.range.bits; 547 state.key = randomKeyInRange(state.rangeStart, width); 548 state.pub = scalarMul(state.key); 549 await renderTarget(); 550 renderProjection(); 551 setPhase("ready"); 552 $("live-status").textContent = 553 "Key generated. The private key, address and public key are in the demonstration panel."; 554}; 555 556// --- init ----------------------------------------------------------------- 557 558renderRangeChoices(); 559renderProjection(); 560setPhase("idle"); 561 562$("run-bench").addEventListener("click", () => { runBench().catch((e) => console.error(e)); }); 563$("generate").addEventListener("click", () => { generate().catch((e) => console.error(e)); }); 564$("run-kangaroo").addEventListener("click", () => { startWalk().catch((e) => console.error(e)); }); 565$("cancel-kangaroo").addEventListener("click", cancelWalk); 566$("regenerate").addEventListener("click", () => { generate().catch((e) => console.error(e)); }); 567 568// Surfaced for anyone reading the source, which the project invites. 569console.log( 570 "%cKrackpot kangaroo demo", 571 "font-weight:bold", 572 "\nEverything runs locally. Source: /src/kangaroo/kangaroo.js" + 573 "\nIt only ever attacks a key this page generated itself." 574);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.