Repository navigation
Expand file tree
/
Copy pathverify.html
More file actions
131 lines (122 loc) · 9.49 KB
/
Copy pathverify.html
File metadata and controls
131 lines (122 loc) · 9.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Verify the assumeUTXO snapshot — SwiftSync, in a tab</title>
<style>
:root { --bg:#fff; --fg:#16181d; --mut:#5b6470; --bd:#e6e8eb; --pan:#fafbfc; --ac:#e8830c; --ac2:#0969da; --good:#1a7f37; --bad:#cf222e; --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; color-scheme:light; }
body { background:var(--bg); color:var(--fg); font:14px/1.6 var(--mono); max-width:760px; margin:0 auto; padding:32px 18px 70px; }
h1 { font-size:20px; } a { color:var(--ac2); }
.sans { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
button { background:var(--pan); color:var(--fg); border:1px solid var(--bd); border-radius:6px; padding:9px 15px; cursor:pointer; font:inherit; }
button:hover { background:#f0f2f4; border-color:var(--ac); } button:disabled { opacity:.45; cursor:default; }
input { font:inherit; border:1px solid var(--bd); border-radius:6px; padding:7px 9px; }
.grid { display:grid; grid-template-columns:repeat(3,1fr); gap:10px; margin:16px 0; }
.card { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--pan); }
.card .k { color:var(--mut); font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
.card .v { font-size:20px; margin-top:3px; }
#bar { height:8px; background:var(--bd); border-radius:4px; overflow:hidden; margin:10px 0; }
#bar>i { display:block; height:100%; width:0; background:var(--good); transition:width .2s; }
#log { background:var(--pan); border:1px solid var(--bd); border-radius:6px; padding:12px; height:140px; overflow:auto; white-space:pre-wrap; margin-top:12px; }
.ok{color:var(--good)} .warn{color:var(--ac)} .err{color:var(--bad)} .dim{color:var(--mut)}
code{background:var(--pan);border:1px solid var(--bd);border-radius:4px;padding:1px 5px;font-size:.92em}
</style>
</head>
<body>
<h1>Verify an assumeUTXO snapshot — with 32 bytes</h1>
<p class="dim sans">Stream a Bitcoin Core <code>dumptxoutset</code> snapshot through the SwiftSync accumulator <b>in a Web Worker</b>
and confirm its commitment equals the one an independent full validation produces. The whole 14M-coin set is processed;
the accumulator state never exceeds <b>32 bytes</b>. <a href="how-it-works.html">how it works</a> · <a href="index.html">the acts</a></p>
<p>
<button id="prefix">▶ Quick check — the 1 MB prefix</button>
<button id="torrent">▶ Verify the full snapshot via WebTorrent (no server)</button>
<button id="full">▶ …or via HTTP</button>
</p>
<p class="dim">snapshot URL: <input id="url" size="34" value="data/utxo-prefix.dat"> expected: <input id="exp" size="20" placeholder="(commitment hex)"></p>
<div id="bar"><i></i></div>
<div class="grid">
<div class="card"><div class="k">coins streamed</div><div class="v" id="n">—</div></div>
<div class="card"><div class="k">data</div><div class="v" id="mb">—</div></div>
<div class="card"><div class="k">accumulator</div><div class="v">32 bytes</div></div>
</div>
<div id="verdict" class="dim">— not run yet —</div>
<div id="log"></div>
<p class="dim sans" style="margin-top:14px;border-top:1px solid var(--bd);padding-top:10px">
This page is a <b>static verifier</b>; the data source is a <b>link parameter</b>, so the demo upgrades from localhost to a
hosted/seeded copy by changing the URL you share — never the code:
<br>· <code>verify.html?url=https://host/snapshot.dat&auto=1</code> (HTTP / object store / a tunnel)
<br>· <code>verify.html?magnet=magnet:?xt=…&ws=https://host/snapshot.dat&auto=1</code> (WebTorrent + webseed)
<br>The <code>expected</code> commitment (default: the testnet4 #141,574 value) is the trust anchor — it comes from
<code>tools/fullchain-node.mjs</code> (a full genesis→tip validation) and <code>tools/swiftsync-commit.mjs</code>, which
agree. The data source is verified <i>against</i> it, so pointing <code>?url=</code> anywhere is safe: wrong data simply fails the check.</p>
<script type="module">
import WebTorrent from './webtorrent.min.js';
const $ = (id) => document.getElementById(id);
const log = (m, c='') => { const t = new Date().toISOString().slice(11,19); $('log').innerHTML += `<span class="dim">${t}</span> <span class="${c}">${m}</span>\n`; $('log').scrollTop = $('log').scrollHeight; };
const fmt = (n) => Number(n).toLocaleString();
const FULL_COMMITMENT = 'af37b01d79b0315c46c585c2d0d8804cf36b62e215dfcb13a4c9b2241e741afa';
let _w = null, _id = 0; const _rpc = new Map(); let _prog = null;
function wcall(cmd, args) {
if (!_w) {
_w = new Worker('node-worker.js', { type: 'module' });
_w.onmessage = (e) => { const d = e.data; if (d.progress) { _prog && _prog(d); return; } const p = _rpc.get(d.id); if (!p) return; _rpc.delete(d.id); d.ok ? p.resolve(d.result) : p.reject(new Error(d.error)); };
_w.onerror = (e) => log('worker error: ' + (e.message || e.filename), 'err');
}
const id = ++_id; _w.postMessage({ id, cmd, args });
return new Promise((res, rej) => _rpc.set(id, { resolve: res, reject: rej }));
}
async function run(url, expected) {
$('go'); document.querySelectorAll('button').forEach(b => b.disabled = true); $('verdict').innerHTML = '';
try {
log(`verifying ${url} …`, 'ok');
_prog = (d) => { if (d.progress !== 'verify') return; $('n').textContent = fmt(d.n); if (d.total) $('bar').firstElementChild.style.width = (100*d.n/d.total).toFixed(1) + '%'; };
const r = await wcall('verifySnapshot', { url, expected });
_prog = null; $('bar').firstElementChild.style.width = '100%';
$('n').textContent = fmt(r.coins); $('mb').textContent = r.mb + ' MB';
const head = `<p>format: Core dumptxoutset · base <code>#snapshot ${r.baseHash.slice(0,16)}…</code> · net ${r.netMagic}</p>` +
`<p>commitment <code>${r.digest.slice(0,40)}…</code> · ${fmt(r.coins)} coins · ${r.mb} MB · ${(r.ms/1000).toFixed(1)}s · 32-byte state</p>`;
$('verdict').innerHTML = head + (expected
? `<p class="${r.matches?'ok':'err'}">${r.matches ? `✅ matches the chain-derived commitment — this <b>is</b> the real testnet4 UTXO set, verified with 32 bytes of state` : '❌ does NOT match the expected commitment'}</p>`
: `<p class="dim">(no expected value given — commitment shown; this is the prefix, a partial set)</p>`);
log(`done: ${fmt(r.coins)} coins → ${r.digest.slice(0,16)}…${expected?(r.matches?' ✓ verified':' ✗ mismatch'):''}`, r.matches===false?'err':'ok');
} catch (e) { $('verdict').innerHTML = `<span class="err">error: ${e.message}</span>`; log('error: ' + e.message, 'err'); console.error(e); }
document.querySelectorAll('button').forEach(b => b.disabled = false);
}
// ── The data source is a LINK PARAMETER, not baked in. The page is a static
// verifier; you upgrade the demo (localhost → tunnel → object store → WebTorrent)
// by changing the URL you share — never the deployed code.
// verify.html?url=https://host/snapshot.dat[&auto=1]
// verify.html?magnet=magnet:?xt=…&ws=https://host/snapshot.dat[&auto=1]
// &expected=<hex> (defaults to the testnet4 #141,574 commitment)
const q = new URLSearchParams(location.search);
const P = { url: q.get('url'), magnet: q.get('magnet'), ws: q.get('ws'), expected: q.get('expected') || FULL_COMMITMENT, auto: q.get('auto') === '1' };
if (P.url) $('url').value = P.url;
$('exp').value = P.expected;
async function webtorrentVerify(magnet, ws, expected) {
document.querySelectorAll('button').forEach(b => b.disabled = true); $('verdict').innerHTML = '';
try {
log('WebTorrent: fetching the snapshot peer-to-peer…', 'ok');
const urlList = []; if (ws) urlList.push(ws); urlList.push(new URL('data/snapshot-full.dat', location.href).href); // webseed(s): param + same-origin
const client = new WebTorrent();
client.on('error', (e) => log('webtorrent: ' + e.message, 'err'));
client.add(magnet, { urlList }, (torrent) => {
const iv = setInterval(() => { $('mb').textContent = (torrent.downloaded / 1048576).toFixed(0) + ' MB'; $('bar').firstElementChild.style.width = (100 * torrent.progress).toFixed(1) + '%'; }, 1000);
torrent.on('done', async () => {
clearInterval(iv); log('download complete — streaming through the accumulator…', 'ok');
const blob = await torrent.files[0].blob(); const u = URL.createObjectURL(blob);
await run(u, expected); URL.revokeObjectURL(u);
});
});
} catch (e) { $('verdict').innerHTML = `<span class="err">error: ${e.message}</span>`; log('error: ' + e.message, 'err'); document.querySelectorAll('button').forEach(b => b.disabled = false); }
}
const magnetSource = async () => P.magnet || (await (await fetch('magnet-snapshot.txt')).text()).trim();
$('prefix').onclick = () => run('data/utxo-prefix.dat', null); // the 1 MB committed prefix — always works on Pages
$('torrent').onclick = async () => { try { webtorrentVerify(await magnetSource(), P.ws, $('exp').value || FULL_COMMITMENT); } catch { $('verdict').innerHTML = '<span class="err">no magnet — pass ?magnet=… or run node seed-snapshot.mjs</span>'; } };
$('full').onclick = () => run($('url').value || 'data/snapshot-full.dat', $('exp').value || FULL_COMMITMENT);
// A shared link just runs: ?url=… (HTTP) or ?magnet=… (WebTorrent), with &auto=1
if (P.auto) { if (P.magnet) $('torrent').click(); else if (P.url) $('full').click(); }
log(P.url || P.magnet ? `source from link: ${P.magnet ? 'WebTorrent magnet' : P.url}` : 'ready — quick-check the prefix, or point ?url= / ?magnet= at any host.', 'dim');
</script>
</body>
</html>