Improve PoW verification animation

This commit is contained in:
satoshi committed 2026-09-02 07:35:37 +03:00
1 parent 3df739e5b3
commit ec42ce9b54
3 files changed
+1637 -601

No files matched your search

File diff suppressed because it is too large. Load diff
+62 -601
View File
@@ -1,8 +1,10 @@
<script lang="ts">
import { onDestroy } from "svelte";
import { selectBestShare } from "../stores/selection.svelte";
import { snap } from "../stores/snapshot.svelte";
import { formatDifficulty, explorerBaseFor } from "../format";
import PowVerifyOverlay from "./PowVerifyOverlay.svelte";
import type { CheckResult } from "./PowVerifyOverlay.svelte";
import { revHex } from "./sha256";
const data = $derived(snap.data!);
const pow = $derived(data.best_share_pow ?? null);
@@ -18,29 +20,9 @@
explorerBaseFor(data.chain?.chain, data.mempool_base_url),
);
// ── Hex helpers ─────────────────────────────────────────────────────
// The raw header is in hashing byte order: every field is serialized
// little-endian, and hashes read "backwards" compared to how explorers
// display them. revHex flips between the two.
function revHex(hex: string): string {
let out = "";
for (let i = hex.length - 2; i >= 0; i -= 2) out += hex.slice(i, i + 2);
return out;
}
function u32LE(hex8: string): number {
return parseInt(revHex(hex8), 16) >>> 0;
}
function hexToBytes(hex: string): Uint8Array {
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
return out;
}
function bytesToHex(b: Uint8Array): string {
return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
}
function hex8(x: number): string {
return (x >>> 0).toString(16).padStart(8, "0");
}
// ── Header field decomposition ──────────────────────────────────────
// 80 bytes: version(4) | prev block(32) | merkle root(32) | time(4) |
@@ -48,10 +30,10 @@
type HeaderField = {
key: string;
name: string;
bytes: string; // byte range label
raw: string; // hex slice as it appears in the raw header (LE)
value: string; // decoded, human-facing
sub: string; // secondary line
bytes: string;
raw: string;
value: string;
sub: string;
};
const fields = $derived.by((): HeaderField[] => {
@@ -113,9 +95,6 @@
});
// ── Coinbase decomposition ──────────────────────────────────────────
// coinbase = coinb1 | extranonce1 | extranonce2 | coinb2. cb1len is
// the byte length of coinb1; the extranonce hex lengths come from the
// strings themselves.
const cbSegments = $derived.by(() => {
if (!pow) return [];
const cb = pow.coinbase;
@@ -132,394 +111,19 @@
const branches = $derived(pow?.merklebranches ?? []);
// ── Pure-JS SHA-256 with introspection hooks ────────────────────────
// crypto.subtle is unavailable outside secure contexts, and dashboards
// are often served over plain HTTP on a LAN — so verification uses a
// self-contained implementation and works everywhere. The hooks expose
// the real message schedule, per-round working registers (with the
// W[t]/K[t] inputs), and the post-block chained state — everything the
// animation shows comes from actually hashing this share's data.
const K = new Uint32Array([
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]);
const INIT_H = [
"6a09e667", "bb67ae85", "3c6ef372", "a54ff53a", "510e527f", "9b05688c", "1f83d9ab", "5be0cd19",
];
function rotr(x: number, n: number): number {
return ((x >>> n) | (x << (32 - n))) >>> 0;
}
type Sha256Hooks = {
schedule?: (block: number, blocks: number, words: string[]) => void;
round?: (block: number, blocks: number, round: number, regs: Uint32Array, wt: number, kt: number) => void;
blockDone?: (block: number, blocks: number, state: string[]) => void;
};
function sha256(msg: Uint8Array, hooks?: Sha256Hooks): Uint8Array {
const len = msg.length;
const total = ((len + 9 + 63) >> 6) << 6;
const padded = new Uint8Array(total);
padded.set(msg);
padded[len] = 0x80;
const dv = new DataView(padded.buffer);
const bits = len * 8;
dv.setUint32(total - 8, Math.floor(bits / 0x100000000));
dv.setUint32(total - 4, bits >>> 0);
const h = new Uint32Array([
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
]);
const w = new Uint32Array(64);
const blocks = total / 64;
for (let off = 0; off < total; off += 64) {
const blockNo = off / 64 + 1;
for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4);
for (let i = 16; i < 64; i++) {
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
}
if (hooks?.schedule) hooks.schedule(blockNo, blocks, Array.from(w.subarray(0, 16), hex8));
let a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];
for (let i = 0; i < 64; i++) {
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
const ch = (e & f) ^ (~e & g);
const t1 = (hh + S1 + ch + K[i] + w[i]) >>> 0;
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
const maj = (a & b) ^ (a & c) ^ (b & c);
const t2 = (S0 + maj) >>> 0;
hh = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
if (hooks?.round) hooks.round(blockNo, blocks, i + 1, new Uint32Array([a, b, c, d, e, f, g, hh]), w[i], K[i]);
}
h[0] = (h[0] + a) >>> 0; h[1] = (h[1] + b) >>> 0; h[2] = (h[2] + c) >>> 0; h[3] = (h[3] + d) >>> 0;
h[4] = (h[4] + e) >>> 0; h[5] = (h[5] + f) >>> 0; h[6] = (h[6] + g) >>> 0; h[7] = (h[7] + hh) >>> 0;
if (hooks?.blockDone) hooks.blockDone(blockNo, blocks, Array.from(h, hex8));
}
const out = new Uint8Array(32);
const ov = new DataView(out.buffer);
for (let i = 0; i < 8; i++) ov.setUint32(i * 4, h[i]);
return out;
}
function sha256d(msg: Uint8Array): Uint8Array {
return sha256(sha256(msg));
}
// Difficulty-D target as 64 hex chars (same math as the analysis page).
const diff1Target = BigInt("0x00000000FFFF0000000000000000000000000000000000000000000000000000");
function targetHex(d: number): string {
if (!d || d <= 0) return "f".repeat(64);
const scale = BigInt(1) << BigInt(48);
const dScaled = BigInt(Math.round(d * Number(scale)));
if (dScaled === BigInt(0)) return "f".repeat(64);
let s = ((diff1Target * scale) / dScaled).toString(16);
while (s.length < 64) s = "0" + s;
return s.slice(0, 64);
}
// ── Verification with animated playback ─────────────────────────────
// A guided tour of the full verification, in chapters. Every value
// shown is taken from the real computation over this share's data.
type CheckResult = { name: string; ok: boolean; detail: string };
type Chapter = { n: number; of: number; title: string };
type PadSeg = { cls: string; hex: string };
type TimelineStep =
| { kind: "chapter"; ch: Chapter; dwell: number }
| { kind: "merkle-init"; txid: string; dwell: number }
| { kind: "merkle-fold"; i: number; total: number; left: string; right: string; result: string; dwell: number }
| { kind: "merkle-done"; root: string; matches: boolean; dwell: number }
| { kind: "padding"; dwell: number }
| { kind: "init-h"; dwell: number }
| { kind: "schedule"; block: number; blocks: number; words: string[]; dwell: number }
| {
kind: "round";
block: number;
blocks: number;
round: number;
pct: number;
regs: string[];
wt: string;
kt: string;
dwell: number;
}
| { kind: "add-h"; block: number; blocks: number; state: string[]; dwell: number }
| { kind: "digest"; label: string; hex: string; dwell: number }
| { kind: "reverse"; dwell: number }
| { kind: "target"; dwell: number }
| { kind: "done" };
let animating = $state(false);
let verified = $state(false);
// ── Verification: handed to the full-screen overlay ─────────────────
let showOverlay = $state(false);
let checks: CheckResult[] = $state([]);
let step: TimelineStep | null = $state(null);
let chapter: Chapter | null = $state(null);
let paused = $state(false);
let speed = $state(1);
let digest1Hex = $state("");
let digest2Hex = $state("");
let verified = $state(false);
let finalHash = $state("");
let padBlock1: PadSeg[] = $state([]);
let padBlock2: PadSeg[] = $state([]);
let shareTargetHex = $state("");
let netTargetHex = $state("");
let pendingChecks: CheckResult[] = [];
let timeline: TimelineStep[] = [];
let nextIndex = 0;
let timer: ReturnType<typeof setTimeout> | null = null;
onDestroy(() => {
if (timer) clearTimeout(timer);
});
// Which rounds to show, and for how long: all of the first 6, every
// 2nd to 16, then every 4th — always including round 64.
function roundStep(round: number): { keep: boolean; dwell: number } {
if (round <= 6) return { keep: true, dwell: 110 };
if (round <= 16) return { keep: round % 2 === 0, dwell: 48 };
return { keep: round % 4 === 0 || round === 64, dwell: 22 };
}
function computeChecks(): CheckResult[] {
if (!pow) return [];
const results: CheckResult[] = [];
const gotHash = revHex(bytesToHex(sha256d(hexToBytes(pow.header))));
results.push({
name: "SHA-256d(header) matches the recorded share hash",
ok: gotHash === pow.hash,
detail: gotHash,
});
let node = sha256d(hexToBytes(pow.coinbase));
const cbTxid = revHex(bytesToHex(node));
for (const b of pow.merklebranches ?? []) {
const buf = new Uint8Array(64);
buf.set(node);
buf.set(hexToBytes(b), 32);
node = sha256d(buf);
}
const rootInternal = bytesToHex(node);
results.push({
name: "Coinbase + merkle branches reproduce the header's merkle root",
ok: rootInternal === pow.header.slice(72, 136),
detail: `coinbase txid ${cbTxid} → root ${revHex(rootInternal)}`,
});
const hv = BigInt("0x" + gotHash);
const scale = BigInt(1000000);
const computedDiff = hv > BigInt(0) ? Number((diff1Target * scale) / hv) / 1000000 : Infinity;
const ratio = pow.sdiff > 0 ? computedDiff / pow.sdiff : 0;
results.push({
name: "Hash value corresponds to the recorded difficulty",
ok: ratio > 0.999 && ratio < 1.001,
detail: `computed ${formatDifficulty(computedDiff)}, recorded ${formatDifficulty(pow.sdiff)}`,
});
return results;
}
// Trace one SHA-256 pass, returning displayable steps grouped by block.
function tracePass(input: Uint8Array): { digest: Uint8Array; perBlock: TimelineStep[][] } {
const schedules: TimelineStep[][] = [];
const rounds: TimelineStep[][] = [];
const addhs: (TimelineStep | null)[] = [];
const digest = sha256(input, {
schedule: (block, blocks, words) => {
schedules[block - 1] = [{ kind: "schedule", block, blocks, words, dwell: 1500 }];
},
round: (block, blocks, round, regs, wt, kt) => {
const rs = roundStep(round);
if (!rs.keep) return;
(rounds[block - 1] ??= []).push({
kind: "round",
block,
blocks,
round,
pct: round / 64,
regs: Array.from(regs, hex8),
wt: hex8(wt),
kt: hex8(kt),
dwell: rs.dwell,
});
},
blockDone: (block, blocks, state) => {
addhs[block - 1] = { kind: "add-h", block, blocks, state, dwell: 1300 };
},
});
const perBlock: TimelineStep[][] = schedules.map((sch, i) => [
...sch,
...(rounds[i] ?? []),
...(addhs[i] ? [addhs[i]!] : []),
]);
return { digest, perBlock };
}
function buildPaddingSegs(header: string): void {
// 80-byte header + 0x80 marker + 39 zero bytes + 8-byte bit length
// (640 = 0x280) = 128 bytes = two 512-bit blocks. The merkle root
// straddles the block boundary — a nice detail worth seeing.
const segs: PadSeg[] = fields.map((f) => ({ cls: `seg-text-${f.key}`, hex: f.raw }));
segs.push({ cls: "pad-seg", hex: "80" + "00".repeat(39) });
segs.push({ cls: "len-seg", hex: "0000000000000280" });
// Split the flat segment list at the 128-hex-char block boundary.
const b1: PadSeg[] = [];
const b2: PadSeg[] = [];
let pos = 0;
for (const s of segs) {
const end = pos + s.hex.length;
if (end <= 128) b1.push(s);
else if (pos >= 128) b2.push(s);
else {
b1.push({ cls: s.cls, hex: s.hex.slice(0, 128 - pos) });
b2.push({ cls: s.cls, hex: s.hex.slice(128 - pos) });
}
pos = end;
}
padBlock1 = b1;
padBlock2 = b2;
}
function startVerify(): void {
if (!pow || animating) return;
pendingChecks = computeChecks();
buildPaddingSegs(pow.header);
shareTargetHex = targetHex(pow.sdiff);
netTargetHex = targetHex(pow.netdiff);
// Merkle chapter data (all internal byte order, as hashed).
const cbTxidInternal = bytesToHex(sha256d(hexToBytes(pow.coinbase)));
const folds: TimelineStep[] = [];
let node = cbTxidInternal;
const bl = pow.merklebranches ?? [];
const foldDwell = Math.max(320, Math.min(900, Math.floor(3600 / Math.max(bl.length, 1))));
for (let i = 0; i < bl.length; i++) {
const buf = new Uint8Array(64);
buf.set(hexToBytes(node));
buf.set(hexToBytes(bl[i]), 32);
const result = bytesToHex(sha256d(buf));
folds.push({
kind: "merkle-fold",
i: i + 1,
total: bl.length,
left: node,
right: bl[i],
result,
dwell: foldDwell,
});
node = result;
}
const rootMatches = node === pow.header.slice(72, 136);
const pass1 = tracePass(hexToBytes(pow.header));
digest1Hex = bytesToHex(pass1.digest);
const pass2 = tracePass(pass1.digest);
digest2Hex = bytesToHex(pass2.digest);
finalHash = revHex(digest2Hex);
// Honor reduced-motion: jump straight to the result.
if (typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
finishVerify();
return;
}
const OF = 8;
const ch = (n: number, title: string): TimelineStep => ({
kind: "chapter",
ch: { n, of: OF, title },
dwell: 1000,
});
timeline = [
ch(1, "Rebuild the merkle root"),
{ kind: "merkle-init", txid: cbTxidInternal, dwell: 1500 },
...folds,
{ kind: "merkle-done", root: node, matches: rootMatches, dwell: 1600 },
ch(2, "Pad the header into two 512-bit blocks"),
{ kind: "padding", dwell: 3400 },
ch(3, "Initialize the hash state"),
{ kind: "init-h", dwell: 2000 },
ch(4, "Compress message block 1"),
...pass1.perBlock[0],
ch(5, "Compress message block 2"),
...pass1.perBlock[1],
{ kind: "digest", label: "First digest — 32 bytes", hex: digest1Hex, dwell: 1500 },
ch(6, "Hash it again — Bitcoin always hashes twice"),
...pass2.perBlock[0].filter((s) => s.kind !== "add-h"),
{ kind: "digest", label: "Raw double-SHA-256 digest", hex: digest2Hex, dwell: 1500 },
ch(7, "Reverse the bytes for display"),
{ kind: "reverse", dwell: 1900 },
ch(8, "Compare against the target"),
{ kind: "target", dwell: 3800 },
{ kind: "done" },
];
animating = true;
verified = false;
paused = false;
checks = [];
play(0);
}
// Global pacing: the authored dwells describe the tour at design
// speed; SLOWDOWN stretches every step uniformly, and chapter
// transitions hold for an extra beat on top so each stage change
// breathes before its content starts.
const SLOWDOWN = 3;
const CHAPTER_EXTRA_MS = 1500;
function play(i: number): void {
const s = timeline[i];
if (!s) {
finishVerify();
return;
}
step = s;
nextIndex = i + 1;
if (s.kind === "chapter") chapter = s.ch;
if (s.kind === "done") {
finishVerify();
return;
}
const dwell = s.dwell * SLOWDOWN + (s.kind === "chapter" ? CHAPTER_EXTRA_MS : 0);
timer = setTimeout(() => play(i + 1), dwell / speed);
}
function togglePause(): void {
if (!animating) return;
if (paused) {
paused = false;
// Re-enter at the step being viewed (with its full dwell), not the
// one after it — pausing is for reading the current step.
play(Math.max(0, nextIndex - 1));
} else {
paused = true;
if (timer) clearTimeout(timer);
timer = null;
}
}
function toggleSpeed(): void {
speed = speed === 1 ? 2 : 1;
}
function finishVerify(): void {
if (timer) clearTimeout(timer);
timer = null;
animating = false;
paused = false;
verified = true;
checks = pendingChecks;
chapter = null;
step = { kind: "done" };
}
const allOk = $derived(verified && checks.every((c) => c.ok));
// Leading zeros of the final hash rendered green, like the analysis page.
function onVerifyComplete(result: CheckResult[], hash: string): void {
checks = result;
finalHash = hash;
verified = true;
}
const finalHashChars = $derived.by(() => {
let leading = true;
return finalHash.split("").map((c) => {
@@ -528,10 +132,6 @@
});
});
const targetGapX = $derived(
pow && pow.sdiff > 0 ? pow.netdiff / pow.sdiff : 0,
);
// ── Copy helper (with non-secure-context fallback) ──────────────────
let copiedKey = $state("");
function copyText(key: string, text: string): void {
@@ -571,10 +171,7 @@
const foundAt = $derived(pow ? new Date(pow.seen_at).toLocaleString() : "");
function onKey(ev: KeyboardEvent): void {
if (ev.key === "Escape") {
if (animating) finishVerify();
else selectBestShare();
}
if (ev.key === "Escape" && !showOverlay) selectBestShare();
}
</script>
@@ -698,6 +295,20 @@
share hash. No trust in this dashboard required.
</p>
<div class="proof-note">
<strong>This is what turns a claim into proof.</strong>
A screenshot of a big difficulty number proves nothing &mdash; anyone can edit one, and
no one can tell from a picture whether the work behind it was ever done. These 80 bytes
can't be faked that way. They are the exact preimage of the share hash, so anyone can
hash them and see for themselves both that the hash is genuine and that finding it took
the work claimed. The header also commits to the
<span class="seg-text-prev">previous block</span>, fixing when the work happened, and
through the <span class="seg-text-merkle">merkle root</span> to the coinbase
transaction, fixing who it pays &mdash; so the same proof can't be lifted from another
miner's share or replayed from a different block. Publish this, and the claim stands on
its own.
</div>
<div class="raw-block-wrap">
<div class="raw-block mono">
{#each fields as f (f.key)}<span class="seg-text-{f.key}">{f.raw}</span>{/each}
@@ -712,190 +323,17 @@
type="button"
class="verify-btn"
class:ok={allOk}
disabled={animating}
onclick={startVerify}
onclick={() => (showOverlay = true)}
>
{animating ? "Verifying…" : verified ? (allOk ? "Verified ✓" : "Verification failed") : "Verify in this browser"}
{verified ? (allOk ? "Verified \u2713 — watch again" : "Verification failed — replay") : "Verify in this browser"}
</button>
<span class="verify-hint">
replays the full verification &mdash; merkle proof, padding, every SHA-256 stage
&mdash; from the real computation, locally
takes over the screen and replays the whole verification &mdash; merkle proof,
padding, every SHA-256 round &mdash; from the real computation, locally
</span>
</div>
{#if animating && step}
<div class="anim-panel">
<div class="anim-controls">
<button type="button" class="ctl-btn" onclick={togglePause}>
{paused ? "▶ Resume" : "❚❚ Pause"}
</button>
<button type="button" class="ctl-btn" class:active={speed === 2} onclick={toggleSpeed}>
2&times;
</button>
<button type="button" class="ctl-btn" onclick={finishVerify}>Skip &rarr;</button>
</div>
{#if chapter}
<div class="chapter-line">
<span class="chapter-num">{chapter.n}/{chapter.of}</span>
<span class="chapter-title">{chapter.title}</span>
</div>
<div class="chapter-dots" aria-hidden="true">
{#each { length: chapter.of } as _, i}
<span class="dot" class:on={i < chapter.n}></span>
{/each}
</div>
{/if}
{#if step.kind === "chapter"}
<div class="chapter-splash">{step.ch.title}</div>
{:else if step.kind === "merkle-init"}
<div class="anim-line">SHA-256d(<span class="cb-en1">coinbase transaction</span>) &rarr; its txid:</div>
<div class="anim-digest mono">{step.txid}</div>
<div class="anim-note">internal byte order, as used while hashing</div>
{:else if step.kind === "merkle-fold"}
<div class="anim-line">
Fold level {step.i}/{step.total} &mdash; concatenate with branch hash, SHA-256d again:
</div>
<div class="fold-grid mono">
<span class="fold-tag">current</span>
<span class="fold-hex">{step.left}</span>
<span class="fold-tag">branch {step.i}</span>
<span class="fold-hex dim">{step.right}</span>
<span class="fold-tag good">&rarr; new</span>
<span class="fold-hex glow">{step.result}</span>
</div>
<div class="anim-note">
each branch hash summarizes every other transaction in its subtree
</div>
{:else if step.kind === "merkle-done"}
<div class="anim-line">Merkle root reproduced:</div>
<div class="anim-digest mono">{step.root}</div>
<div class="final-match" class:ok={step.matches} class:fail={!step.matches}>
{step.matches
? "= the merkle root committed in the header ✓"
: "≠ the header's merkle root!"}
</div>
{:else if step.kind === "padding"}
<div class="anim-line">
80 header bytes + <span class="pad-seg mono">0x80</span> marker + 39 zero bytes +
<span class="len-seg mono">640</span>-bit length = 128 bytes:
</div>
<div class="pad-blocks">
<div class="pad-block">
<div class="pad-label">message block 1 &middot; 512 bits</div>
<div class="pad-hex mono">
{#each padBlock1 as s, i (i)}<span class={s.cls}>{s.hex}</span>{/each}
</div>
</div>
<div class="pad-block">
<div class="pad-label">message block 2 &middot; 512 bits</div>
<div class="pad-hex mono">
{#each padBlock2 as s, i (i)}<span class={s.cls}>{s.hex}</span>{/each}
</div>
</div>
</div>
<div class="anim-note">
note the <span class="seg-text-merkle">merkle root</span> straddling the block
boundary — SHA-256 processes 64 bytes at a time regardless of field layout
</div>
{:else if step.kind === "init-h"}
<div class="anim-line">Eight 32-bit registers start from the SHA-256 constants:</div>
<div class="regs">
{#each INIT_H as v, i (i)}
<div class="reg">
<span class="reg-name">H{i}</span>
<span class="reg-val mono">{v}</span>
</div>
{/each}
</div>
<div class="anim-note">
the fractional parts of the square roots of the first eight primes — "nothing up
my sleeve" numbers
</div>
{:else if step.kind === "schedule"}
<div class="anim-line">
Block {step.block}/{step.blocks} becomes sixteen 32-bit words w<sub>0</sub>…w<sub>15</sub>:
</div>
<div class="words">
{#each step.words as w, i (i)}
<div class="word mono"><span class="word-idx">w{i}</span>{w}</div>
{/each}
</div>
<div class="anim-note">
rounds 17–64 consume words expanded from these (σ<sub>0</sub>/σ<sub>1</sub> mixing)
</div>
{:else if step.kind === "round"}
<div class="anim-progress">
<div class="anim-progress-fill" style="width:{(step.pct * 100).toFixed(1)}%"></div>
</div>
<div class="anim-round">
block {step.block}/{step.blocks} &middot; compression round
<span class="mono">{String(step.round).padStart(2, "0")}</span>/64 &middot;
W[t]=<span class="mono">{step.wt}</span> K[t]=<span class="mono">{step.kt}</span>
</div>
<div class="regs">
{#each step.regs as r, i (i)}
<div class="reg" class:fresh={i === 0 || i === 4}>
<span class="reg-name">{"abcdefgh"[i]}</span>
<span class="reg-val mono">{r}</span>
</div>
{/each}
</div>
<div class="anim-note formula mono">
T1 = h + Σ1(e) + Ch(e,f,g) + K[t] + W[t] &nbsp;·&nbsp; T2 = Σ0(a) + Maj(a,b,c)
&nbsp;&rarr;&nbsp; a = T1+T2, e = d+T1, rest shift down
</div>
{:else if step.kind === "add-h"}
<div class="anim-line">
64 rounds done &mdash; the result is <em>added</em> into the running state
(Davies&ndash;Meyer), chaining block {step.block} forward:
</div>
<div class="regs">
{#each step.state as v, i (i)}
<div class="reg">
<span class="reg-name">H{i}</span>
<span class="reg-val mono">{v}</span>
</div>
{/each}
</div>
{:else if step.kind === "digest"}
<div class="anim-line">{step.label}:</div>
<div class="anim-digest mono">{step.hex}</div>
{:else if step.kind === "reverse"}
<div class="anim-digest mono dim">{digest2Hex}</div>
<div class="anim-flip" aria-hidden="true">&darr; reverse the 32 bytes &mdash; Bitcoin displays hashes little-endian</div>
<div class="anim-digest mono">
{#each finalHashChars as ch}<span class:fz={ch.zero}>{ch.c}</span>{/each}
</div>
{:else if step.kind === "target"}
<div class="target-rows mono">
<div class="target-row">
<span class="target-tag">your hash</span>
<span class="target-hex">
{#each finalHashChars as ch}<span class:fz={ch.zero}>{ch.c}</span>{/each}
</span>
</div>
<div class="target-row">
<span class="target-tag">share target</span>
<span class="target-hex dim">{shareTargetHex}</span>
</div>
<div class="target-row">
<span class="target-tag">network target</span>
<span class="target-hex dim">{netTargetHex}</span>
</div>
</div>
<div class="anim-note">
hash &lt; share target &mdash; that's what made it a
{formatDifficulty(pow.sdiff)} share. The network target is another
{targetGapX >= 1000 ? formatDifficulty(targetGapX) : targetGapX.toFixed(1)}&times;
lower: beat it, and this header would have been block #{pow.height.toLocaleString()}.
</div>
{/if}
</div>
{/if}
{#if verified && !animating}
{#if verified}
<div class="result-panel">
<div class="anim-line">Computed block header hash</div>
<div class="final-hash mono">
@@ -904,12 +342,12 @@
<div class="final-match" class:ok={finalHash === pow.hash} class:fail={finalHash !== pow.hash}>
{finalHash === pow.hash
? "= the recorded share hash — proof of work is genuine"
: "≠ the recorded share hash!"}
: "\u2260 the recorded share hash!"}
</div>
<ul class="check-list">
{#each checks as c (c.name)}
<li class:ok={c.ok} class:fail={!c.ok}>
<span class="check-mark">{c.ok ? "✓" : "✗"}</span>
<span class="check-mark">{c.ok ? "\u2713" : "\u2717"}</span>
<span>
{c.name}
<span class="check-detail mono">{c.detail}</span>
@@ -917,7 +355,6 @@
</li>
{/each}
</ul>
<button type="button" class="replay-btn" onclick={startVerify}>&#8635; Replay</button>
</div>
{/if}
@@ -997,6 +434,15 @@
{/if}
</section>
{#if showOverlay && pow}
<PowVerifyOverlay
{pow}
{fields}
onclose={() => (showOverlay = false)}
oncomplete={onVerifyComplete}
/>
{/if}
<style>
.page {
display: flex;
@@ -1253,6 +699,21 @@
border-radius: 0 0 9px 9px;
}
/* Why the raw bytes matter */
.proof-note {
margin-bottom: 1rem;
padding: 0.9rem 1.1rem;
border-left: 3px solid var(--accent);
border-radius: 0 8px 8px 0;
background: rgba(255, 122, 58, 0.07);
color: var(--fg-dim);
line-height: 1.65;
font-size: 0.92rem;
}
.proof-note strong {
color: var(--fg);
}
/* ── Raw hex blocks ── */
.raw-block-wrap {
position: relative;
+142
View File
@@ -0,0 +1,142 @@
// Self-contained SHA-256.
//
// crypto.subtle is unavailable outside secure contexts and dashboards are
// often served over plain HTTP on a LAN, so proof-of-work verification
// uses this rather than the platform digest. The optional hooks expose
// the real message schedule, the working registers after every
// compression round, and the chained state after each block — which is
// what the verification animation renders. Nothing shown there is
// simulated; it is all this function's actual intermediate state.
const K = new Uint32Array([
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]);
/** The eight initial hash values, as hex — √2…√19 fractional parts. */
export const INIT_H = [
"6a09e667", "bb67ae85", "3c6ef372", "a54ff53a", "510e527f", "9b05688c", "1f83d9ab", "5be0cd19",
];
function rotr(x: number, n: number): number {
return ((x >>> n) | (x << (32 - n))) >>> 0;
}
export type Sha256Hooks = {
schedule?: (block: number, blocks: number, words: string[]) => void;
round?: (
block: number,
blocks: number,
round: number,
regs: Uint32Array,
wt: number,
kt: number,
) => void;
blockDone?: (block: number, blocks: number, state: string[]) => void;
};
export function hex8(x: number): string {
return (x >>> 0).toString(16).padStart(8, "0");
}
export function sha256(msg: Uint8Array, hooks?: Sha256Hooks): Uint8Array {
const len = msg.length;
const total = ((len + 9 + 63) >> 6) << 6;
const padded = new Uint8Array(total);
padded.set(msg);
padded[len] = 0x80;
const dv = new DataView(padded.buffer);
const bits = len * 8;
dv.setUint32(total - 8, Math.floor(bits / 0x100000000));
dv.setUint32(total - 4, bits >>> 0);
const h = new Uint32Array([
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
]);
const w = new Uint32Array(64);
const blocks = total / 64;
for (let off = 0; off < total; off += 64) {
const blockNo = off / 64 + 1;
for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4);
for (let i = 16; i < 64; i++) {
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
}
if (hooks?.schedule) hooks.schedule(blockNo, blocks, Array.from(w.subarray(0, 16), hex8));
let a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];
for (let i = 0; i < 64; i++) {
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
const ch = (e & f) ^ (~e & g);
const t1 = (hh + S1 + ch + K[i] + w[i]) >>> 0;
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
const maj = (a & b) ^ (a & c) ^ (b & c);
const t2 = (S0 + maj) >>> 0;
hh = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
if (hooks?.round) {
hooks.round(blockNo, blocks, i + 1, new Uint32Array([a, b, c, d, e, f, g, hh]), w[i], K[i]);
}
}
h[0] = (h[0] + a) >>> 0; h[1] = (h[1] + b) >>> 0; h[2] = (h[2] + c) >>> 0; h[3] = (h[3] + d) >>> 0;
h[4] = (h[4] + e) >>> 0; h[5] = (h[5] + f) >>> 0; h[6] = (h[6] + g) >>> 0; h[7] = (h[7] + hh) >>> 0;
if (hooks?.blockDone) hooks.blockDone(blockNo, blocks, Array.from(h, hex8));
}
const out = new Uint8Array(32);
const ov = new DataView(out.buffer);
for (let i = 0; i < 8; i++) ov.setUint32(i * 4, h[i]);
return out;
}
export function sha256d(msg: Uint8Array): Uint8Array {
return sha256(sha256(msg));
}
export function hexToBytes(hex: string): Uint8Array {
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
return out;
}
export function bytesToHex(b: Uint8Array): string {
return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
}
/** Reverse a hex string byte-wise: internal <-> display byte order. */
export function revHex(hex: string): string {
let out = "";
for (let i = hex.length - 2; i >= 0; i -= 2) out += hex.slice(i, i + 2);
return out;
}
/** Difficulty-1 target, the numerator of the difficulty formula. */
export const DIFF1_TARGET = BigInt(
"0x00000000FFFF0000000000000000000000000000000000000000000000000000",
);
/** The 256-bit target for a given difficulty, as 64 hex chars. */
export function targetHex(d: number): string {
if (!d || d <= 0) return "f".repeat(64);
const scale = BigInt(1) << BigInt(48);
const dScaled = BigInt(Math.round(d * Number(scale)));
if (dScaled === BigInt(0)) return "f".repeat(64);
let s = ((DIFF1_TARGET * scale) / dScaled).toString(16);
while (s.length < 64) s = "0" + s;
return s.slice(0, 64);
}
/** Difficulty represented by a display-order hash. */
export function diffOfHash(displayHash: string): number {
const hv = BigInt("0x" + displayHash);
if (hv === BigInt(0)) return Infinity;
const scale = BigInt(1000000);
return Number((DIFF1_TARGET * scale) / hv) / 1000000;
}