Skip to content

Instantly share code, notes, and snippets.

@bst27
Created September 2, 2025 17:42
Show Gist options
  • Select an option

  • Save bst27/451dcb7f764143fa9642b2df03461542 to your computer and use it in GitHub Desktop.

Select an option

Save bst27/451dcb7f764143fa9642b2df03461542 to your computer and use it in GitHub Desktop.
Proof of Work (PoW) example for the web browser
<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>
@bst27
Copy link
Author

bst27 commented Sep 2, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment