Created
September 2, 2025 17:42
-
-
Save bst27/451dcb7f764143fa9642b2df03461542 to your computer and use it in GitHub Desktop.
Proof of Work (PoW) example for the web browser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div class="app"> | |
| <h1>Browser Proof of Work (SHA-256)</h1> | |
| <div class="row"> | |
| <label>Data</label> | |
| <input id="data" value="Hello World" /> | |
| </div> | |
| <div class="row"> | |
| <label>Difficulty (leading zeros)</label> | |
| <input id="difficulty" type="number" min="1" max="8" value="4" /> | |
| </div> | |
| <div class="buttons"> | |
| <button id="start">Start</button> | |
| <button id="stop" disabled>Stop</button> | |
| <button class="preset" data-diff="3">Easy (3)</button> | |
| <button class="preset" data-diff="4">Demo (4)</button> | |
| <button class="preset" data-diff="5">Harder (5)</button> | |
| </div> | |
| <div class="status"> | |
| <div class="progress" id="progress" hidden> | |
| <div class="bar"></div> | |
| </div> | |
| <div class="stats"> | |
| <div><strong>Status:</strong> <span id="status">Idle</span></div> | |
| <div><strong>Attempts:</strong> <span id="attempts">0</span></div> | |
| <div><strong>Elapsed:</strong> <span id="elapsed">0.00s</span></div> | |
| <div><strong>Hashrate:</strong> <span id="hps">0 H/s</span></div> | |
| <div><strong>Target prefix:</strong> <code id="prefix">0000</code></div> | |
| </div> | |
| </div> | |
| <div class="result card" id="result" hidden> | |
| <h3>Solution</h3> | |
| <div><strong>Nonce:</strong> <span id="nonce">—</span></div> | |
| <div><strong>Hash:</strong> <code id="hash">—</code></div> | |
| <div><strong>Verifies:</strong> <span id="verifies">—</span></div> | |
| </div> | |
| <details class="card"> | |
| <summary>What’s happening?</summary> | |
| <p> | |
| We brute-force a number (<em>nonce</em>) so that | |
| <code>SHA-256(data + "|" + nonce)</code> starts with a certain count of leading zeros. | |
| Hard to find, easy to check. Increase difficulty to make it exponentially harder. | |
| </p> | |
| </details> | |
| </div> | |
| <style> | |
| :root { --bg:#0b0f14; --fg:#e6edf3; --muted:#9fb0c3; --card:#121923; --accent:#5da0ff; --accent2:#64d2ff; } | |
| * { box-sizing: border-box; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial; } | |
| body { margin: 0; background: linear-gradient(160deg, #0b0f14, #0b0f14 60%, #0e1520); color: var(--fg); } | |
| .app { max-width: 820px; margin: 32px auto; padding: 24px; } | |
| h1 { font-size: 24px; margin: 0 0 16px; letter-spacing: .2px; } | |
| .row { display: grid; grid-template-columns: 220px 1fr; gap: 12px; align-items: center; margin: 12px 0; } | |
| label { color: var(--muted); } | |
| input { width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid #263244; background: #0f1622; color: var(--fg); } | |
| .buttons { display: flex; gap: 8px; margin: 16px 0 8px; flex-wrap: wrap; } | |
| button { padding: 10px 14px; border-radius: 12px; border: 1px solid #2a3c55; background: #122033; color: var(--fg); cursor: pointer; } | |
| button:hover { border-color: #34517a; } | |
| button:disabled { opacity: .5; cursor: not-allowed; } | |
| .preset { background: #111a29; } | |
| .status { margin: 12px 0; } | |
| .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 8px 16px; margin-top: 10px; } | |
| code { background: #0f1622; padding: 2px 6px; border-radius: 6px; } | |
| .card { border: 1px solid #203049; background: #0f1622; padding: 14px; border-radius: 14px; margin-top: 14px; } | |
| .progress { height: 10px; border-radius: 999px; background: #0e1a2a; overflow: hidden; border: 1px solid #1d2a40; } | |
| .progress .bar { width: 40%; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--accent), var(--accent2)); animation: slide 1.1s linear infinite; } | |
| @keyframes slide { 0%{ transform: translateX(-100%);} 100%{ transform: translateX(260%);} } | |
| </style> | |
| <script> | |
| const $ = (id) => document.getElementById(id); | |
| const enc = new TextEncoder(); | |
| function bufToHex(buf) { | |
| const v = new Uint8Array(buf); | |
| let s = ''; | |
| for (let i = 0; i < v.length; i++) s += v[i].toString(16).padStart(2, '0'); | |
| return s; | |
| } | |
| async function sha256Hex(str) { | |
| const digest = await crypto.subtle.digest('SHA-256', enc.encode(str)); | |
| return bufToHex(digest); | |
| } | |
| function formatHps(n) { | |
| if (!isFinite(n)) return '0 H/s'; | |
| if (n >= 1e9) return (n/1e9).toFixed(2) + ' GH/s'; | |
| if (n >= 1e6) return (n/1e6).toFixed(2) + ' MH/s'; | |
| if (n >= 1e3) return (n/1e3).toFixed(2) + ' kH/s'; | |
| return Math.max(0, n|0) + ' H/s'; | |
| } | |
| function verify({ data, nonce, difficulty, hash }) { | |
| const prefix = '0'.repeat(difficulty); | |
| return hash.startsWith(prefix) && hash === window._lastCalc; | |
| } | |
| let running = false; | |
| async function mine({ data, difficulty }, onProgress) { | |
| const prefix = '0'.repeat(difficulty); | |
| const start = performance.now(); | |
| let attempts = 0; | |
| let nonce = 0; | |
| const batch = 300; // try this many hashes per UI tick | |
| while (running) { | |
| const batchStart = performance.now(); | |
| for (let i = 0; i < batch; i++) { | |
| const candidate = `${data}|${nonce}`; | |
| const hash = await sha256Hex(candidate); | |
| window._lastCalc = hash; // tiny help for verify() | |
| attempts++; | |
| if (hash.startsWith(prefix)) { | |
| const elapsed = (performance.now() - start) / 1000; | |
| const hps = attempts / elapsed; | |
| onProgress({ attempts, elapsed, hps, nonce, hash, done: true }); | |
| return { data, nonce, hash, attempts, elapsed, difficulty }; | |
| } | |
| nonce++; | |
| if (!running) break; | |
| } | |
| const elapsed = (performance.now() - start) / 1000; | |
| const hps = attempts / Math.max(0.001, elapsed); | |
| onProgress({ attempts, elapsed, hps, nonce, done: false }); | |
| // Yield to UI | |
| if (performance.now() - batchStart > 12) await new Promise(r => requestAnimationFrame(r)); | |
| } | |
| throw new Error('Aborted'); | |
| } | |
| function setUIRunning(on) { | |
| running = on; | |
| $('start').disabled = on; | |
| $('stop').disabled = !on; | |
| $('progress').hidden = !on; | |
| $('status').textContent = on ? 'Mining…' : 'Idle'; | |
| } | |
| function updateStats({ attempts, elapsed, hps, nonce, hash, done }) { | |
| $('attempts').textContent = attempts.toLocaleString('de-DE'); | |
| $('elapsed').textContent = elapsed.toFixed(2) + 's'; | |
| $('hps').textContent = formatHps(hps); | |
| if (done) { | |
| $('nonce').textContent = nonce; | |
| $('hash').textContent = hash; | |
| $('result').hidden = false; | |
| $('verifies').textContent = 'checking…'; | |
| // quick re-check against target | |
| sha256Hex(`${$('data').value}|${nonce}`).then(h => { | |
| const ok = h === hash && h.startsWith('0'.repeat(Number($('difficulty').value))); | |
| $('verifies').textContent = ok ? '✅ Yes' : '❌ No'; | |
| }); | |
| $('status').textContent = 'Found solution'; | |
| } | |
| } | |
| $('start').addEventListener('click', async () => { | |
| $('result').hidden = true; | |
| setUIRunning(true); | |
| const data = $('data').value; | |
| const difficulty = Math.max(1, Math.min(8, Number($('difficulty').value || 4))); | |
| $('prefix').textContent = '0'.repeat(difficulty); | |
| try { | |
| await mine({ data, difficulty }, updateStats); | |
| } catch (e) { | |
| // aborted or error | |
| } finally { | |
| setUIRunning(false); | |
| } | |
| }); | |
| $('stop').addEventListener('click', () => setUIRunning(false)); | |
| document.querySelectorAll('.preset').forEach(btn => { | |
| btn.addEventListener('click', () => { $('difficulty').value = btn.dataset.diff; }); | |
| }); | |
| </script> |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Live demo: https://codepen.io/bst27/pen/Ggpwzbb