-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcache.html
More file actions
377 lines (364 loc) · 24.9 KB
/
Copy pathcache.html
File metadata and controls
377 lines (364 loc) · 24.9 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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>bitcoin-kernel - your node</title>
<meta name="description" content="The slice of the Bitcoin chain stored and verified on this device. Each block was fetched once, checked by the engine, and kept so it never has to be fetched again.">
<meta property="og:title" content="bitcoin-kernel - your node">
<meta property="og:description" content="The blocks your browser has cached and verified, ready to serve.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://bitcoin-kernel.com/cache.html">
<meta property="og:image" content="https://bitcoin-kernel.com/og.png?v=4">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="bitcoin-kernel - your node">
<meta name="twitter:description" content="The blocks your browser has cached and verified, ready to serve.">
<meta name="twitter:image" content="https://bitcoin-kernel.com/og.png?v=4">
<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}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif;-webkit-font-smoothing:antialiased}
a{color:var(--ac2);text-decoration:none}a:hover{text-decoration:underline}
.wrap{max-width:760px;margin:0 auto;padding:0 1.5rem}
nav{border-bottom:1px solid var(--bd)}nav .wrap{display:flex;align-items:center;gap:1.3rem;height:56px;max-width:940px}
nav .brand{font-weight:700;letter-spacing:-.3px;margin-right:auto;color:var(--fg);font-size:1rem;text-decoration:none}nav .brand b{color:var(--ac)}
nav a{color:var(--mut);font-size:.9rem;font-weight:500}nav a.on{color:var(--fg)}
header.hero{padding:2.4rem 0 1rem;text-align:center}
h1{font-size:2.1rem;line-height:1.08;letter-spacing:-1px;margin:0;font-weight:800}
.sub{font-size:1.05rem;color:#37414d;margin:.8rem auto 0;max-width:36rem}
button{font:inherit;font-weight:600;cursor:pointer;border:1px solid var(--bd);background:#fff;border-radius:10px;padding:.55rem 1rem;transition:border-color .12s,background .12s}
button:hover{border-color:var(--ac)}
button.danger:hover{border-color:var(--bad);color:var(--bad)}
button.primary{background:var(--ac);color:#fff;border-color:var(--ac)}button.primary:hover{background:#d2760a}
button:disabled{opacity:.55;cursor:default}
.controls{display:flex;justify-content:center;align-items:center;gap:.9rem;margin:1.4rem 0 0}
.gstat{text-align:center;margin:.7rem 0 0;font:.85rem var(--mono);color:var(--mut);min-height:1.2em}
.peering{display:flex;flex-wrap:wrap;gap:.6rem;justify-content:center;align-items:center;margin:1rem 0 0}
.ptoggle{font-size:.9rem;color:var(--fg);display:flex;align-items:center;gap:.4rem;cursor:pointer}.ptoggle input{accent-color:var(--ac);cursor:pointer}
.tinput{font:.8rem var(--mono);border:1px solid var(--bd);border-radius:8px;padding:.45rem .6rem;width:18rem;max-width:70vw;color:var(--fg)}.tinput:focus{outline:none;border-color:var(--ac)}
.pstat{width:100%;text-align:center;font:.8rem var(--mono);color:var(--mut);min-height:1.1em}
#card{margin:1.6rem 0 0;border:1px solid var(--bd);border-radius:16px;overflow:hidden;box-shadow:0 8px 30px rgba(16,18,29,.06)}
.hero2{padding:1.7rem 1.4rem 1.4rem;text-align:center;background:linear-gradient(180deg,#fff7ee,#fff);border-bottom:1px solid var(--bd)}
.hero2 .big{font:800 3rem var(--mono);letter-spacing:-2px;color:var(--ac);line-height:1}
.hero2 .lab{font-size:.95rem;color:#37414d;margin-top:.35rem}
.hero2 .bar{height:.6rem;background:var(--bd);border-radius:99px;overflow:hidden;margin:1rem auto .4rem;max-width:24rem}
.hero2 .bar i{display:block;height:100%;width:0;background:var(--ac);border-radius:99px;transition:width .9s cubic-bezier(.2,.85,.25,1)}
.hero2 .meta{font:.8rem var(--mono);color:var(--mut)}
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(6.2rem,1fr));gap:.1rem;background:var(--bd);border-top:1px solid var(--bd)}
.stat{background:#fff;padding:.9rem 1rem}.stat .n{font:800 1.4rem var(--mono);letter-spacing:-.5px}.stat .l{font-size:.74rem;color:var(--mut);text-transform:uppercase;letter-spacing:.03em}
.stat .n.g{color:var(--good)}.stat .n.a{color:var(--ac)}.stat .n.b{color:var(--ac2)}
.seclab{padding:.95rem 1.4rem .15rem;font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;color:var(--mut);border-top:1px solid var(--bd);display:flex;justify-content:space-between;align-items:center}
.blocks{padding:.1rem 0}
.brow{display:flex;align-items:center;gap:.7rem;padding:.5rem 1.4rem;border-top:1px solid var(--bd);font:.8rem var(--mono)}
.brow .h{font-weight:700;width:5.5rem;flex-shrink:0}
.brow .v{flex-shrink:0;width:1.2rem;text-align:center}.brow .v.ok{color:var(--good)}.brow .v.no{color:var(--mut)}
.brow .sz{color:var(--mut);width:5rem;flex-shrink:0}
.brow .hash{color:var(--mut);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.empty{padding:2rem 1.4rem;text-align:center;color:var(--mut)}
.setting{padding:.7rem 1.4rem 1rem}
.setting .srow{display:flex;justify-content:space-between;align-items:baseline;font-size:.92rem}
.setting .srow b{font:800 1.05rem var(--mono);color:var(--ac)}
.setting .smeta{font:.78rem var(--mono);color:var(--mut)}
.setting input[type=range]{width:100%;margin:.5rem 0 .3rem;accent-color:var(--ac);cursor:pointer}
.note{margin:1.1rem 0 0;padding:.7rem 1rem;background:var(--pan);border:1px solid var(--bd);border-radius:10px;font-size:.85rem;color:var(--mut);line-height:1.5}.note b{color:var(--fg)}
footer{margin:3rem 0 2rem;text-align:center;font-size:.85rem;color:var(--mut)}
</style>
</head>
<body>
<nav><div class="wrap">
<a href="./index.html" class="brand">bitcoin<b>·</b>kernel</a>
<a href="./index.html">Test suite</a>
<a href="./random.html">Verify a block</a>
<a href="./cache.html" class="on">Your node</a>
<a href="./spec.html">Specification</a>
<a href="https://github.com/bitcoin-kernel/bitcoin-kernel.github.io">Source ↗</a>
</div></nav>
<header class="hero"><div class="wrap">
<h1>Your node.</h1>
<p class="sub">The slice of the Bitcoin chain stored on this device. Each block was fetched once, verified by the engine, and kept here so it never has to be fetched again. As bitcoin-kernel grows toward a shared mesh, this is what your node can serve to others.</p>
<div class="controls"><button id="grow" class="primary">+ Grow your node</button><label class="ptoggle"><input type="checkbox" id="auto"> Auto-grow</label><label class="ptoggle"><input type="checkbox" id="dagtoggle"> Verify DAG</label></div>
<div id="gstat" class="gstat"></div>
<div class="peering">
<label class="ptoggle"><input type="checkbox" id="peer"> Peer with others</label>
<input id="tracker" class="tinput" placeholder="wss://your-tracker/.webrtc" spellcheck="false">
<div id="peerstat" class="pstat"></div>
</div>
</div></header>
<div class="wrap">
<div id="card"></div>
<p class="note"><b>Peering (experimental).</b> Turn on "Peer with others" and your browser joins a swarm on a tracker you provide and serves the blocks it holds to other peers directly — the chain held by the swarm, not one explorer. There is no default tracker; bring your own (a JSS <code>/.webrtc</code> endpoint), and the <code>?tracker=</code> link is the invitation — share it and whoever opens it joins your swarm. Every block is still verified by hash on arrival, so a peer can never hand you a bad one. Storage is shared with the browser and bounded; old blocks drop as new ones arrive.</p>
</div>
<footer>An independent JavaScript implementation. <a href="./index.html">bitcoin-kernel</a></footer>
<script type="module">
import { cache } from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/web/cache.js';
import { Sources, CacheSource, ExplorerSource, PeerSource } from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/web/sources.js';
import { Mesh, swarmHash } from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/web/mesh.js';
import { Codec } from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/kernel/codec/codec.js';
import { BlockEngine } from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/kernel/codec/blocks.js';
import { bytesToHex } from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/kernel/codec/hash.js';
import { setVerifyBackend } from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/kernel/codec/secp256k1.js';
import { loadSecpWasm, wasmBackend } from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/kernel/wasm/secp-wasm.js';
import { verifyDag } from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/web/dag.js';
import core from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/kernel/schema/core.js';
import proof from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/kernel/schema/proof.js';
import chain from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/kernel/schema/chain.js';
import validate from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/kernel/schema/validate.js';
import script from 'https://cdn.jsdelivr.net/gh/bitcoin-kernel/kernel@v0.0.2/packages/kernel/schema/script.js';
const $ = (id) => document.getElementById(id);
const fmt = (n) => Number(n).toLocaleString();
const codec = new Codec(core, proof);
const be = BlockEngine.fromSchemas(codec, chain, validate, script, 'btc:mainnet');
const wasmReady = loadSecpWasm().then(() => setVerifyBackend(wasmBackend)).catch(() => {}); // real secp for DAG script+sig checks
const EXPLORERS = ['https://mempool.space', 'https://blockstream.info'];
let mesh = null; // active when peering is on
let sources = new Sources([new CacheSource(cache), new ExplorerSource(EXPLORERS)]);
function rebuildSources() { // cache -> [peers] -> explorer
const list = [new CacheSource(cache)];
if (mesh) list.push(new PeerSource(mesh));
list.push(new ExplorerSource(EXPLORERS));
sources = new Sources(list);
}
// --- peering (BYO tracker, no default hard-coded) ---
const SWARM_LABEL = 'bitcoin-kernel:mainnet'; // a label, not infra — same swarm = same peers
const TRACKER_KEY = 'bk:tracker';
const host = (u) => { try { return new URL(u).host; } catch { return u; } };
function savedTracker() {
const q = new URLSearchParams(location.search).get('tracker');
return (q || localStorage.getItem(TRACKER_KEY) || '').trim();
}
function peerStat(s) {
const el = $('peerstat'); if (!el) return;
if (!mesh) { el.textContent = ''; return; }
el.innerHTML = s && s.connected
? `peering via <b>${host($('tracker').value)}</b> · <b style="color:var(--good)">${s.peers}</b> peer${s.peers === 1 ? '' : 's'} · <a id="invite" href="#">copy invite link</a>`
: `connecting to <b>${host($('tracker').value)}</b>…`;
const inv = $('invite');
if (inv) inv.onclick = (e) => { e.preventDefault(); const link = location.origin + location.pathname + '?tracker=' + encodeURIComponent($('tracker').value); navigator.clipboard.writeText(link).then(() => { inv.textContent = 'invite link copied'; }); };
}
async function setPeering(on) {
const url = ($('tracker').value || savedTracker()).trim();
if (on) {
if (!/^wss?:\/\//.test(url)) { $('peerstat').textContent = 'enter a wss:// tracker URL first'; $('peer').checked = false; return; }
$('tracker').value = url; localStorage.setItem(TRACKER_KEY, url); localStorage.setItem('bk:peering', '1');
await cache.init();
const swarm = await swarmHash(SWARM_LABEL);
mesh = new Mesh({ tracker: url, swarm, serve: (h) => cache.get(h), inventory: () => cache.entries().map((e) => [e.height, e.hash]), onstatus: peerStat });
mesh.start(); rebuildSources(); peerStat(mesh.status());
} else {
localStorage.setItem('bk:peering', '0');
if (mesh) { mesh.stop(); mesh = null; }
rebuildSources(); peerStat(null);
}
}
const tipHeight = () => sources.tipHeight().catch(() => null);
// Grow your node: fetch the highest block within the recent window that we don't
// hold yet, verify it (proof of work, merkle, structure), and store it — so the
// node fills and follows a contiguous window from the tip down, one block a click.
let growing = false;
// Grow one block. Pass a tip hint to avoid a per-block explorer call while
// auto-growing. Returns { status:'added'|'current'|'failed'|'error'|'busy',
// from, tip } so the caller (manual click or the auto loop) can pace itself.
async function growOne(tipHint, invHint) {
if (growing) return { status: 'busy' };
growing = true;
const st = $('gstat');
try {
let tip = tipHint;
if (tip == null) { st.textContent = 'finding the chain tip…'; tip = await sources.tipHeight(); }
await cache.init();
const have = new Set(cache.entries().map((e) => e.height));
// Swarm-aware fill: prefer the highest missing block a PEER already holds —
// we get its hash from the inventory, so no explorer lookup at all. Only the
// racing tip (no peer has it yet) falls back to the explorer.
const inv = invHint || (mesh ? await mesh.swarmInventory() : new Map());
let target = null, knownHash = null;
for (let h = tip; h > tip - cache.CAP && h >= 0; h--) { if (!have.has(h) && inv.has(h)) { target = h; knownHash = inv.get(h); break; } }
if (target == null) { for (let h = tip; h > tip - cache.CAP && h >= 0; h--) { if (!have.has(h)) { target = h; break; } } }
if (target == null) {
st.innerHTML = `<b style="color:var(--good)">✓ your node is current</b> — holding the latest ${fmt(Math.min(cache.CAP, tip + 1))} blocks`;
return { status: 'current', tip };
}
st.textContent = `fetching block ${fmt(target)}…`;
const hash = knownHash || await sources.hashForHeight(target);
const got = await sources.getBlock(hash);
st.textContent = `verifying block ${fmt(target)}…`;
const block = codec.decode('Block', bytesToHex(got.bytes));
const txids = block.transactions.map((t) => codec.txid(t));
const ok = codec.checkProofOfWork(block.header)
&& codec.merkleRoot(txids) === block.header.merkleRoot
&& be.validateBlockStructure(block).results.every((r) => r.ok !== false);
if (!ok) { st.innerHTML = `<b style="color:var(--bad)">✗ block ${fmt(target)} failed verification</b> — not stored`; return { status: 'failed', tip, inv }; }
await cache.put(hash, target, got.bytes);
await cache.markVerified(hash);
await cache.indexBlock(target, txids);
const via = got.from === 'a peer' ? ' · <b style="color:var(--ac)">⚡ from a peer</b>' : ` · <span style="color:var(--mut)">from ${got.from}</span>`;
st.innerHTML = `<b style="color:var(--good)">✓ verified & added block ${fmt(target)}</b> · ${fmt(block.transactions.length)} transactions${via}`;
return { status: 'added', from: got.from, height: target, tip, inv };
} catch (e) {
st.textContent = 'Error: ' + e.message;
return { status: 'error', error: e.message };
} finally {
growing = false;
}
}
// --- auto-grow: keep filling the window, paced by source ---
const PEER_DELAY = 1000; // fast off the peer mesh
const EXPLORER_DELAY = 12000; // very slow off an explorer — never hammer it
const CURRENT_RECHECK = 180000; // window full: re-check the tip every ~3 min
const ERROR_BACKOFF = 8000;
let autoOn = false, autoWake = null;
const autoSleep = (ms) => new Promise((res) => { const t = setTimeout(res, ms); autoWake = () => { clearTimeout(t); autoWake = null; res(); }; });
function setAuto(on) {
autoOn = on; localStorage.setItem('bk:autogrow', on ? '1' : '0');
if (on) autoLoop(); else if (autoWake) autoWake();
}
async function autoLoop() {
let tip = null, inv = null;
while (autoOn) {
if (tip == null) { try { tip = await sources.tipHeight(); } catch { await autoSleep(ERROR_BACKOFF); continue; } inv = null; }
if (inv == null && mesh) inv = await mesh.swarmInventory(); // refresh what peers hold (once per cycle / after explorer)
const r = await growOne(tip, inv);
inv = r.inv || inv;
if (!autoOn) break;
await load(tip); // refresh stats without another explorer call
if (r.status === 'current') { tip = null; await autoSleep(CURRENT_RECHECK); } // re-fetch tip later, then follow
else if (r.status === 'error' || r.status === 'failed') { inv = null; await autoSleep(ERROR_BACKOFF); }
else { if (r.from !== 'a peer') inv = null; await autoSleep(r.from === 'a peer' ? PEER_DELAY : EXPLORER_DELAY); } // re-poll inventory after an explorer fetch
}
}
// --- DAG verification: re-check the resolvable slice in the background ---
// Coverage and counts climb as the window deepens; runs after each load/grow.
// Re-entrant-safe: a fresh run cancels the one in flight (the window changed).
let dag = null; // last completed result, painted into the stats
let dagRun = 0; // generation token; a newer run supersedes older ones
let dagBusy = false;
let dagTimer = null;
let dagEnabled = localStorage.getItem('bk:dagverify') === '1'; // off by default — a full-window re-verify is heavy
// Debounced: load() fires every ~1s while auto-growing; wait for the window to
// settle before the (full-window) re-verify, so we run once per quiet period.
function scheduleDag(ms = 4000) { if (!dagEnabled) return; clearTimeout(dagTimer); dagTimer = setTimeout(runDag, ms); }
function setDag(on) {
dagEnabled = on; localStorage.setItem('bk:dagverify', on ? '1' : '0');
if (on) scheduleDag(0); // run now
else { clearTimeout(dagTimer); dagRun++; dag = null; paintDag(); } // bump generation to bail any in-flight run
}
async function runDag() {
if (!dagEnabled) return;
if (dagBusy) { dagRun++; return; } // signal the in-flight run to bail; it reschedules
const stats = cache.stats();
if (stats.n < 2) { dag = null; paintDag(); return; } // need overlap for in-window spends
dagBusy = true;
const gen = ++dagRun;
try {
await wasmReady;
const res = await verifyDag({
cache, codec, be,
shouldStop: () => gen !== dagRun,
onProgress: (p) => { if (gen === dagRun && p.phase === 'verify') { dag = { ...p, partial: true }; paintDag(); } },
});
if (res && gen === dagRun) { dag = res; paintDag(); }
} catch {}
finally {
dagBusy = false;
if (dagRun !== gen) runDag(); // window changed mid-run — go again
}
}
function paintDag() {
const card = $('card'); if (!card) return;
const nEl = $('dagn'), capEl = $('dagcap');
if (!dagEnabled) {
if (nEl) nEl.textContent = '—';
if (capEl) capEl.innerHTML = `<span style="color:var(--mut)">DAG verification off · toggle <b>Verify DAG</b> to script+sig-check the in-window transaction graph</span>`;
return;
}
if (nEl) nEl.textContent = dag ? fmt(dag.verified) : '…';
if (!capEl) return;
if (!dag) { capEl.innerHTML = `<span style="color:var(--mut)">verifying the in-window DAG…</span>`; return; }
if (dag.failed > 0) {
capEl.innerHTML = `<b style="color:var(--bad)">✗ ${fmt(dag.failed)} input${dag.failed === 1 ? '' : 's'} failed verification</b> — at block ${fmt(dag.failures[0].height)}, please report`;
return;
}
const pct = dag.resolvable ? Math.round(dag.verified / dag.resolvable * 100) : 0;
const cov = dag.inputs ? (dag.resolvable / dag.inputs * 100) : 0;
const covTxt = cov >= 1 ? cov.toFixed(0) : cov.toFixed(1);
capEl.innerHTML = `${dag.partial ? 'verifying… ' : ''}<b style="color:var(--good)">${fmt(dag.verified)}</b> input${dag.verified === 1 ? '' : 's'} script+sig verified `
+ `· ${covTxt}% of this window's ${fmt(dag.inputs)} inputs spend coins also in the window`
+ (dag.skipped ? ` · ${fmt(dag.skipped)} skipped (unknown/taproot-partial)` : '');
}
function render(s, q, tip, list, txn, max, avg) {
const mb = (s.bytes / 1e6).toFixed(s.bytes >= 1e7 ? 0 : 1);
const winPct = Math.min(100, s.n / cache.CAP * 100);
const usedGB = q.quota ? (q.usage / 1e9).toFixed(2) : null;
const quotaGB = q.quota ? (q.quota / 1e9).toFixed(0) : null;
const cap = cache.CAP, sliderMax = Math.max(cap, max || 2000), capGB = (cap * avg / 1e9).toFixed(2);
const rows = list.length ? list.map((e) => `
<div class="brow">
<span class="h"><a href="./random.html?height=${e.height}">${fmt(e.height)}</a></span>
<span class="v ${e.verified ? 'ok' : 'no'}">${e.verified ? '✓' : '○'}</span>
<span class="sz">${(e.size / 1e6).toFixed(2)} MB</span>
<span class="hash">${e.hash}</span>
</div>`).join('') : `<div class="empty">No blocks cached yet. <a href="./random.html">Verify a block</a> and it will be stored here.</div>`;
$('card').innerHTML = `
<div class="hero2">
<div class="big">${fmt(s.n)}</div>
<div class="lab">block${s.n === 1 ? '' : 's'} cached on this device${tip ? ` · out of ${fmt(tip)} in the chain` : ''}</div>
<div class="bar"><i id="winfill"></i></div>
<div class="meta">${s.n} / ${cache.CAP} block cache window${usedGB ? ` · ${usedGB} GB of ${quotaGB} GB browser storage used` : ''}</div>
</div>
<div class="stats">
<div class="stat"><div class="n a">${fmt(s.n)}</div><div class="l">Blocks</div></div>
<div class="stat"><div class="n g">${fmt(s.verified)}</div><div class="l">Verified</div></div>
<div class="stat"><div class="n">${mb}<span style="font-size:.7rem;color:var(--mut);font-weight:400"> MB</span></div><div class="l">On device</div></div>
<div class="stat"><div class="n b">${fmt(txn || 0)}</div><div class="l">Tx indexed</div></div>
<div class="stat"><div class="n g" id="dagn">—</div><div class="l">DAG inputs ✓</div></div>
<div class="stat"><div class="n b">${tip ? fmt(tip) : '—'}</div><div class="l">Chain height</div></div>
</div>
<div id="dagcap" class="gstat" style="padding:.55rem 1.4rem 0;text-align:left"></div>
<div class="seclab">Node size</div>
<div class="setting">
<div class="srow"><span>Keep up to <b id="capval">${fmt(cap)}</b> blocks</span><span class="smeta" id="capsize">≈ ${capGB} GB</span></div>
<input type="range" id="capslider" min="10" max="${sliderMax}" step="10" value="${cap}">
<div class="smeta">${quotaGB ? `your browser allows ~${quotaGB} GB` : 'storage limit unknown'}${cap >= sliderMax ? ' · at your limit' : ''} · fill it with Grow your node, or peers once peering is on</div>
</div>
<div class="seclab"><span>Cached blocks</span>${list.length ? `<button id="clear" class="danger" style="font-size:.72rem;padding:.25rem .6rem">Clear cache</button>` : ''}</div>
<div class="blocks">${rows}</div>`;
requestAnimationFrame(() => { const f = $('winfill'); if (f) f.style.width = winPct + '%'; });
paintDag(); // re-apply the DAG result after every re-render
const cb = $('clear');
if (cb) cb.onclick = async () => { if (confirm('Remove all cached blocks from this device?')) { await cache.clear(); load(); } };
const sl = $('capslider');
if (sl) {
sl.oninput = () => { const n = +sl.value; $('capval').textContent = fmt(n); $('capsize').textContent = `≈ ${(n * avg / 1e9).toFixed(2)} GB`; };
sl.onchange = async () => { await cache.setCap(+sl.value); load(); };
}
}
// tipHint skips the explorer tip lookup (used during auto-grow to avoid a call per block)
async function load(tipHint) {
await cache.init();
const s = cache.stats(), list = cache.entries(), txn = await cache.txCount();
const avg = cache.avgBlockSize(), max = await cache.maxBlocks();
if (tipHint !== undefined) { render(s, await cache.quota(), tipHint, list, txn, max, avg); scheduleDag(); return; }
render(s, await cache.quota(), null, list, txn, max, avg); // paint instantly from local data
scheduleDag(1500); // verify the DAG slice shortly after a manual load
const tip = await tipHeight(); // then fill in the chain height
if (tip != null) render(s, await cache.quota(), tip, list, txn, max, avg);
}
$('grow').onclick = async () => { await growOne(); load(); };
$('auto').onchange = () => setAuto($('auto').checked);
$('dagtoggle').onchange = () => setDag($('dagtoggle').checked);
$('peer').onchange = () => setPeering($('peer').checked);
// Prefill the tracker (from ?tracker= or the saved one). A shared ?tracker= link
// is an explicit invitation, so opening one auto-enables peering; a cold visit
// stays off until you toggle it.
$('tracker').value = savedTracker();
const invited = new URLSearchParams(location.search).has('tracker');
if (savedTracker() && (invited || localStorage.getItem('bk:peering') === '1')) {
$('peer').checked = true; setPeering(true);
}
if (localStorage.getItem('bk:autogrow') === '1') { $('auto').checked = true; setAuto(true); }
if (dagEnabled) $('dagtoggle').checked = true; // persisted opt-in; load() will run it
load();
</script>
</body>
</html>