Improve PoW verification animation
This commit is contained in:
3 files changed
+1637
-601
No files matched your search
File diff suppressed because it is too large.
Load diff
+62
-601
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy } from "svelte";
|
|
||||||
import { selectBestShare } from "../stores/selection.svelte";
|
import { selectBestShare } from "../stores/selection.svelte";
|
||||||
import { snap } from "../stores/snapshot.svelte";
|
import { snap } from "../stores/snapshot.svelte";
|
||||||
import { formatDifficulty, explorerBaseFor } from "../format";
|
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 data = $derived(snap.data!);
|
||||||
const pow = $derived(data.best_share_pow ?? null);
|
const pow = $derived(data.best_share_pow ?? null);
|
||||||
@@ -18,29 +20,9 @@
|
|||||||
explorerBaseFor(data.chain?.chain, data.mempool_base_url),
|
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 {
|
function u32LE(hex8: string): number {
|
||||||
return parseInt(revHex(hex8), 16) >>> 0;
|
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 ──────────────────────────────────────
|
// ── Header field decomposition ──────────────────────────────────────
|
||||||
// 80 bytes: version(4) | prev block(32) | merkle root(32) | time(4) |
|
// 80 bytes: version(4) | prev block(32) | merkle root(32) | time(4) |
|
||||||
@@ -48,10 +30,10 @@
|
|||||||
type HeaderField = {
|
type HeaderField = {
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
bytes: string; // byte range label
|
bytes: string;
|
||||||
raw: string; // hex slice as it appears in the raw header (LE)
|
raw: string;
|
||||||
value: string; // decoded, human-facing
|
value: string;
|
||||||
sub: string; // secondary line
|
sub: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fields = $derived.by((): HeaderField[] => {
|
const fields = $derived.by((): HeaderField[] => {
|
||||||
@@ -113,9 +95,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Coinbase decomposition ──────────────────────────────────────────
|
// ── 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(() => {
|
const cbSegments = $derived.by(() => {
|
||||||
if (!pow) return [];
|
if (!pow) return [];
|
||||||
const cb = pow.coinbase;
|
const cb = pow.coinbase;
|
||||||
@@ -132,394 +111,19 @@
|
|||||||
|
|
||||||
const branches = $derived(pow?.merklebranches ?? []);
|
const branches = $derived(pow?.merklebranches ?? []);
|
||||||
|
|
||||||
// ── Pure-JS SHA-256 with introspection hooks ────────────────────────
|
// ── Verification: handed to the full-screen overlay ─────────────────
|
||||||
// crypto.subtle is unavailable outside secure contexts, and dashboards
|
let showOverlay = $state(false);
|
||||||
// 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);
|
|
||||||
let checks: CheckResult[] = $state([]);
|
let checks: CheckResult[] = $state([]);
|
||||||
let step: TimelineStep | null = $state(null);
|
let verified = $state(false);
|
||||||
let chapter: Chapter | null = $state(null);
|
|
||||||
let paused = $state(false);
|
|
||||||
let speed = $state(1);
|
|
||||||
let digest1Hex = $state("");
|
|
||||||
let digest2Hex = $state("");
|
|
||||||
let finalHash = $state("");
|
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));
|
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(() => {
|
const finalHashChars = $derived.by(() => {
|
||||||
let leading = true;
|
let leading = true;
|
||||||
return finalHash.split("").map((c) => {
|
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) ──────────────────
|
// ── Copy helper (with non-secure-context fallback) ──────────────────
|
||||||
let copiedKey = $state("");
|
let copiedKey = $state("");
|
||||||
function copyText(key: string, text: string): void {
|
function copyText(key: string, text: string): void {
|
||||||
@@ -571,10 +171,7 @@
|
|||||||
const foundAt = $derived(pow ? new Date(pow.seen_at).toLocaleString() : "");
|
const foundAt = $derived(pow ? new Date(pow.seen_at).toLocaleString() : "");
|
||||||
|
|
||||||
function onKey(ev: KeyboardEvent): void {
|
function onKey(ev: KeyboardEvent): void {
|
||||||
if (ev.key === "Escape") {
|
if (ev.key === "Escape" && !showOverlay) selectBestShare();
|
||||||
if (animating) finishVerify();
|
|
||||||
else selectBestShare();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -698,6 +295,20 @@
|
|||||||
share hash. No trust in this dashboard required.
|
share hash. No trust in this dashboard required.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div class="proof-note">
|
||||||
|
<strong>This is what turns a claim into proof.</strong>
|
||||||
|
A screenshot of a big difficulty number proves nothing — 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 — 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-wrap">
|
||||||
<div class="raw-block mono">
|
<div class="raw-block mono">
|
||||||
{#each fields as f (f.key)}<span class="seg-text-{f.key}">{f.raw}</span>{/each}
|
{#each fields as f (f.key)}<span class="seg-text-{f.key}">{f.raw}</span>{/each}
|
||||||
@@ -712,190 +323,17 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="verify-btn"
|
class="verify-btn"
|
||||||
class:ok={allOk}
|
class:ok={allOk}
|
||||||
disabled={animating}
|
onclick={() => (showOverlay = true)}
|
||||||
onclick={startVerify}
|
|
||||||
>
|
>
|
||||||
{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>
|
</button>
|
||||||
<span class="verify-hint">
|
<span class="verify-hint">
|
||||||
replays the full verification — merkle proof, padding, every SHA-256 stage
|
takes over the screen and replays the whole verification — merkle proof,
|
||||||
— from the real computation, locally
|
padding, every SHA-256 round — from the real computation, locally
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if animating && step}
|
{#if verified}
|
||||||
<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×
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ctl-btn" onclick={finishVerify}>Skip →</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>) → 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} — 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">→ 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 · 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 · 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} · compression round
|
|
||||||
<span class="mono">{String(step.round).padStart(2, "0")}</span>/64 ·
|
|
||||||
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] · T2 = Σ0(a) + Maj(a,b,c)
|
|
||||||
→ a = T1+T2, e = d+T1, rest shift down
|
|
||||||
</div>
|
|
||||||
{:else if step.kind === "add-h"}
|
|
||||||
<div class="anim-line">
|
|
||||||
64 rounds done — the result is <em>added</em> into the running state
|
|
||||||
(Davies–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">↓ reverse the 32 bytes — 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 < share target — that's what made it a
|
|
||||||
{formatDifficulty(pow.sdiff)} share. The network target is another
|
|
||||||
{targetGapX >= 1000 ? formatDifficulty(targetGapX) : targetGapX.toFixed(1)}×
|
|
||||||
lower: beat it, and this header would have been block #{pow.height.toLocaleString()}.
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if verified && !animating}
|
|
||||||
<div class="result-panel">
|
<div class="result-panel">
|
||||||
<div class="anim-line">Computed block header hash</div>
|
<div class="anim-line">Computed block header hash</div>
|
||||||
<div class="final-hash mono">
|
<div class="final-hash mono">
|
||||||
@@ -904,12 +342,12 @@
|
|||||||
<div class="final-match" class:ok={finalHash === pow.hash} class:fail={finalHash !== pow.hash}>
|
<div class="final-match" class:ok={finalHash === pow.hash} class:fail={finalHash !== pow.hash}>
|
||||||
{finalHash === pow.hash
|
{finalHash === pow.hash
|
||||||
? "= the recorded share hash — proof of work is genuine"
|
? "= the recorded share hash — proof of work is genuine"
|
||||||
: "≠ the recorded share hash!"}
|
: "\u2260 the recorded share hash!"}
|
||||||
</div>
|
</div>
|
||||||
<ul class="check-list">
|
<ul class="check-list">
|
||||||
{#each checks as c (c.name)}
|
{#each checks as c (c.name)}
|
||||||
<li class:ok={c.ok} class:fail={!c.ok}>
|
<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>
|
<span>
|
||||||
{c.name}
|
{c.name}
|
||||||
<span class="check-detail mono">{c.detail}</span>
|
<span class="check-detail mono">{c.detail}</span>
|
||||||
@@ -917,7 +355,6 @@
|
|||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
<button type="button" class="replay-btn" onclick={startVerify}>↻ Replay</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -997,6 +434,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{#if showOverlay && pow}
|
||||||
|
<PowVerifyOverlay
|
||||||
|
{pow}
|
||||||
|
{fields}
|
||||||
|
onclose={() => (showOverlay = false)}
|
||||||
|
oncomplete={onVerifyComplete}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.page {
|
.page {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1253,6 +699,21 @@
|
|||||||
border-radius: 0 0 9px 9px;
|
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 hex blocks ── */
|
||||||
.raw-block-wrap {
|
.raw-block-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user