upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/scripts/build-explorer.py
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
commitceae3d076b5ad11ee84729ae43f14e9312896646 (patch)
tree7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /scripts/build-explorer.py
parentbe63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff)
parent66e0b9158d050c46b1ff2509faeee3f4573cac1c (diff)
Bring main onto the line this project actually ships
main had drifted to a history with no ancestor in common with the tree we build and deploy. It sat at be63e83 (2026-08-09) with 28 commits; the shipped line, deploy/soak-2026-08-21, had 383. The two shared nothing -- git merge-base returns no result at all -- so anyone cloning this repository landed on a default branch that was neither current nor related to the running node. The 28 commits on main were audited before this merge: every one is already present in the tree by content, including the two that are hardest to see because they were rewritten afterwards -- the payout reorg-safety work (PayoutBlockStillOnChain, since evolved to re-check on every retry rather than once at maturity) and the nostr relay certificate verification (SSL_set1_host, now with a Windows root-store path). Nothing on main is lost by this merge. Recorded as a merge rather than a force-push: main only ever moves forward, its old history stays reachable as the first parent, and this is revertible. The tree is taken wholesale from the merged branch, so main's content is now exactly what was built and tested. What this brings onto main: - compactblock: BIP152-style compact block relay ported from upstream 1.2.23, primitives and unit test only, with the RAND_bytes failure path closed and the reconstruction allocation bounded. - upstream/main 17741d9: isolated testnet parameters and genesis, optional 2 MB large pages for RandomX. Mainnet genesis verified byte-identical through upstream's BuildGenesisBlock() refactor. - deploy: every image built on bookworm so binaries run on Debian 12, and the container build now honours the RandomX and libsecp256k1 pins it had been silently ignoring since before 1.2.15. Gates at this tree: 15/15 selftests, compactblock's 17 checks, consensus constants unchanged against e4f898c apart from the pre-existing MAX_BLOCK_SIZE_GEN, mainnet genesis intact, money-log check ok.
Diffstat (limited to 'scripts/build-explorer.py')
-rwxr-xr-xscripts/build-explorer.py376
1 files changed, 376 insertions, 0 deletions
diff --git a/scripts/build-explorer.py b/scripts/build-explorer.py
new file mode 100755
index 0000000..dbb8e17
--- /dev/null
+++ b/scripts/build-explorer.py
@@ -0,0 +1,376 @@
1#!/usr/bin/env python3
2"""Build a static Bitflash block explorer from local blk*.dat files."""
3
4import argparse
5import hashlib
6import json
7import shutil
8import sys
9from pathlib import Path
10
11import bitflash_chain as chain
12
13
14ADDRESS_VERSION = 25
15B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
16
17
18def b58check(payload):
19 v = bytes([ADDRESS_VERSION]) + payload
20 v += chain.sha256d(v)[:4]
21 n = int.from_bytes(v, "big")
22 s = ""
23 while n > 0:
24 n, r = divmod(n, 58)
25 s = B58[r] + s
26 for byte in v:
27 if byte == 0:
28 s = "1" + s
29 else:
30 break
31 return s
32
33
34def hash160(b):
35 return hashlib.new("ripemd160", hashlib.sha256(b).digest()).digest()
36
37
38def spk_info(script_hex):
39 try:
40 spk = bytes.fromhex(script_hex)
41 except ValueError:
42 return {"type": "malformed", "hex": script_hex[:80]}
43 if (
44 len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xA9
45 and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xAC
46 ):
47 return {"type": "p2pkh", "address": b58check(spk[3:23])}
48 if len(spk) >= 35 and spk[-1] == 0xAC:
49 if spk[0] == 0x41 and len(spk) == 67:
50 pk = spk[1:66]
51 return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))}
52 if spk[0] == 0x21 and len(spk) == 35:
53 pk = spk[1:34]
54 return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))}
55 return {"type": "other", "hex": script_hex[:80]}
56
57
58def explorer_tx(tx):
59 vin = []
60 for txin in tx["vin"]:
61 coinbase = (
62 txin["prevout_hash"] == "0" * 64
63 and txin["prevout_n"] == 0xFFFFFFFF
64 )
65 vin.append({
66 "coinbase": coinbase,
67 "prev": None if coinbase else txin["prevout_hash"],
68 "vout": None if coinbase else txin["prevout_n"],
69 })
70 vout = []
71 for out in tx["vout"]:
72 vout.append(dict({"value": out["value_satoshis"]}, **spk_info(out["script_pub_key_hex"])))
73 return {"txid": tx["txid"], "vin": vin, "vout": vout, "size": len(chain.ser_tx(tx))}
74
75
76def parse_args(argv):
77 ap = argparse.ArgumentParser(description=__doc__)
78 ap.add_argument("--datadir", help="Bitflash data directory containing blk*.dat")
79 ap.add_argument("--max-blocks", type=int, default=0,
80 help="number of blocks to scan; 0 means all block files")
81 ap.add_argument("paths", nargs="+",
82 help="with --datadir: <out-dir>; otherwise: <blk*.dat>... <out-dir>")
83 args = ap.parse_args(argv)
84 if args.datadir:
85 if len(args.paths) != 1:
86 raise SystemExit("usage with --datadir: build-explorer.py --datadir DATADIR <out-dir>")
87 args.block_files = None
88 args.out_dir = args.paths[0]
89 else:
90 if len(args.paths) < 2:
91 raise SystemExit("usage: build-explorer.py <blk*.dat>... <out-dir>")
92 args.block_files = args.paths[:-1]
93 args.out_dir = args.paths[-1]
94 return args
95
96
97def load_blocks(args):
98 if args.datadir:
99 return chain.read_blocks(args.datadir, args.max_blocks)
100 return chain.read_blocks_from_files(args.block_files, args.max_blocks)
101
102
103def build_explorer(raw_blocks, out_dir):
104 try:
105 blocks = chain.select_main_chain(raw_blocks)
106 except chain.ParseError as e:
107 raise SystemExit(str(e))
108 out = Path(out_dir)
109 (out / "block").mkdir(parents=True, exist_ok=True)
110
111 summaries = []
112 for height, block in enumerate(blocks):
113 txs = [explorer_tx(tx) for tx in block["transactions"]]
114 detail = {
115 "height": height,
116 "hash": block["hash"],
117 "prev": block["previous_hash"],
118 "merkleRoot": block["merkle_root"],
119 "version": block["version"],
120 "time": block["time"],
121 "bits": int(block["bits"], 16),
122 "nonce": block["nonce"],
123 "size": block["size"],
124 "txs": txs,
125 }
126 cb = txs[0]["vout"][0] if txs and txs[0]["vout"] else {}
127 summaries.append({
128 "height": height,
129 "hash": block["hash"],
130 "time": block["time"],
131 "nTx": len(txs),
132 "size": block["size"],
133 "cbValue": cb.get("value", 0),
134 "cbAddr": cb.get("address", ""),
135 })
136 (out / "block" / ("%d.json" % height)).write_text(
137 json.dumps(detail, sort_keys=True, separators=(",", ":")) + "\n",
138 encoding="ascii",
139 )
140
141 summaries.sort(key=lambda x: -x["height"])
142 (out / "blocks.json").write_text(
143 json.dumps({
144 "network": "bitflash",
145 "tipHeight": len(blocks) - 1,
146 "count": len(blocks),
147 "blocks": summaries,
148 }, sort_keys=True, separators=(",", ":")) + "\n",
149 encoding="ascii",
150 )
151 (out / "index.html").write_text(INDEX_HTML, encoding="utf-8")
152 (out / "style.css").write_text(STYLE_CSS, encoding="utf-8")
153 (out / "explorer.js").write_text(EXPLORER_JS, encoding="utf-8")
154 logo = Path(__file__).resolve().parents[1] / "docs" / "logo.png"
155 if logo.exists():
156 shutil.copyfile(logo, out / "logo.png")
157 return len(blocks), len(blocks) - 1
158
159
160def main(argv):
161 args = parse_args(argv)
162 raw_blocks = load_blocks(args)
163 if not raw_blocks:
164 raise SystemExit("no Bitflash blocks parsed")
165 count, tip = build_explorer(raw_blocks, args.out_dir)
166 print("explorer built: %d main-chain blocks, tip %d, into %s" % (count, tip, args.out_dir))
167 return 0
168
169
170INDEX_HTML = r"""<!doctype html>
171<html lang="en">
172<head>
173<meta charset="utf-8">
174<meta name="viewport" content="width=device-width, initial-scale=1">
175<title>Bitflash Block Explorer</title>
176<meta name="description" content="Read-only Bitflash block explorer generated from local blk*.dat files.">
177<link rel="icon" href="logo.png" type="image/png">
178<link rel="stylesheet" href="style.css">
179<script src="explorer.js" defer></script>
180</head>
181<body>
182<header class="topbar">
183 <a class="brand" href="https://bitflash.network/"><img src="logo.png" width="22" height="22" alt="">Bitflash</a>
184 <nav aria-label="Bitflash sites">
185 <a href="https://bitflash.network/">home</a>
186 <a href="https://git.bitflash.network/bitflash/bitflash">code</a>
187 <a href="https://docs.bitflash.network/">docs</a>
188 <a href="https://releases.bitflash.network/">downloads</a>
189 <a href="https://explorer.bitflash.network/" aria-current="page">explorer</a>
190 <a href="https://status.bitflash.network/">status</a>
191 </nav>
192 <span class="meta" id="tip"></span>
193</header>
194<div class="wrap">
195 <h1>Block explorer <button class="link" id="home" hidden>back to blocks</button></h1>
196 <p class="note">Read-only view of the main chain: blocks, their transactions, and the coinbase payout. There is no mempool and no per-address history here.</p>
197 <div id="listview">
198 <input id="q" placeholder="Search by height or block hash">
199 <div class="scroll"><table>
200 <thead><tr><th>Height</th><th>Time (UTC)</th><th>Txs</th><th>Size</th><th>Coinbase</th><th>To</th></tr></thead>
201 <tbody id="rows"></tbody>
202 </table></div>
203 <p class="mut" id="more"></p>
204 </div>
205 <div id="detail" hidden></div>
206</div>
207</body>
208</html>
209"""
210
211
212STYLE_CSS = r""":root{
213 color-scheme: light dark;
214 --bg:#fff; --bg-2:#f6f6f6; --fg:#111; --dim:#6b6b6b; --line:#d7d7d7; --line-hard:#111;
215 --accent:#0b7a34; --accent-2:#12a047; --bar:3rem;
216 --mono: ui-monospace,"Cascadia Mono","Segoe UI Mono",Consolas,"DejaVu Sans Mono","Liberation Mono","Courier New",monospace;
217}
218@media (prefers-color-scheme: dark){:root{
219 --bg:#000; --bg-2:#0b0b0b; --fg:#dcdcdc; --dim:#8a8a8a; --line:#242424; --line-hard:#4a4a4a;
220 --accent:#2ee06a; --accent-2:#6bff9f;
221}}
222*{box-sizing:border-box;}
223html,body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.55 var(--mono);-webkit-text-size-adjust:100%;}
224a{color:var(--accent);}
225.topbar{display:flex;align-items:center;gap:1rem;flex-wrap:wrap;min-height:var(--bar);
226 padding:.5rem clamp(.9rem,2.5vw,3rem);border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:5;}
227.brand{display:flex;align-items:center;gap:.6rem;text-decoration:none;color:var(--fg);font-weight:bold;}
228.brand img{width:22px;height:22px;display:block;}
229.topbar nav{display:flex;flex-wrap:wrap;gap:.15rem 1.4rem;font-size:.86rem;}
230.topbar nav a{text-decoration:none;color:var(--dim);}
231.topbar nav a::before{content:"/";color:var(--line-hard);margin-right:.3rem;}
232.topbar nav a:hover{color:var(--fg);text-decoration:underline;}
233.topbar nav a[aria-current="page"]{color:var(--accent);font-weight:bold;}
234.topbar nav a[aria-current="page"]::before{color:var(--accent);}
235.topbar .meta{margin-left:auto;font-size:.76rem;color:var(--dim);letter-spacing:.06em;}
236.wrap{padding:1.4rem clamp(.9rem,2.5vw,3rem) 4rem;}
237h1{font-size:1rem;margin:0 0 .3rem;letter-spacing:.02em;}
238h2{font-size:.95rem;margin:1.2rem 0 .4rem;}
239.mut{color:var(--dim);}
240.note{color:var(--dim);font-size:.82rem;max-width:60rem;margin:.2rem 0 1rem;}
241input{width:100%;max-width:28rem;padding:.5rem .7rem;border:1px solid var(--line);background:var(--bg-2);color:var(--fg);font:inherit;font-size:.86rem;}
242input:focus{outline:none;border-color:var(--accent);}
243.scroll{overflow-x:auto;}
244table{width:100%;border-collapse:collapse;margin-top:.8rem;font-size:.86rem;}
245th,td{text-align:left;padding:.5rem .6rem;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums;}
246th{font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;color:var(--dim);font-weight:normal;}
247tr.blk{cursor:pointer;}
248tr.blk:hover td{background:var(--bg-2);}
249code{font-size:.8rem;word-break:break-all;color:var(--fg);}
250.card{border:1px solid var(--line);background:var(--bg-2);padding:.9rem 1rem;margin-top:.8rem;}
251.kv{display:grid;grid-template-columns:11rem 1fr;gap:.25rem .8rem;font-size:.82rem;}
252.kv div:nth-child(odd){color:var(--dim);}
253.tx{border-top:1px solid var(--line);padding:.7rem 0;}
254.io{display:grid;grid-template-columns:1fr 1fr;gap:1rem;}
255@media (max-width:640px){.io{grid-template-columns:1fr;}.kv{grid-template-columns:7rem 1fr;}.topbar .meta{margin-left:0;width:100%;}}
256.pill{display:inline-block;font-size:.68rem;padding:.05rem .5rem;border:1px solid var(--line);color:var(--dim);text-transform:uppercase;letter-spacing:.05em;}
257button.link{background:none;border:none;color:var(--accent);cursor:pointer;font:inherit;padding:0;text-decoration:underline;}
258"""
259
260
261EXPLORER_JS = r"""
262var SUM = [], SHOWN = 200;
263function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; }
264function ts(t){ return new Date(t*1000).toISOString().replace('T',' ').replace('.000Z',' UTC'); }
265function short(h){ return h ? h.slice(0,10)+"..."+h.slice(-6) : "-"; }
266function E(tag, cls, text){
267 var e = document.createElement(tag);
268 if(cls) e.className = cls;
269 if(text !== undefined) e.textContent = text;
270 return e;
271}
272function code(text){ return E('code', '', text); }
273function clear(e){ while(e.firstChild) e.removeChild(e.firstChild); }
274function append(parent){
275 for(var i=1;i<arguments.length;i++) parent.appendChild(arguments[i]);
276 return parent;
277}
278function row(cells, cls){
279 var tr = E('tr', cls || '');
280 cells.forEach(function(c){ append(tr, E('td', '', c)); });
281 return tr;
282}
283function renderList(filter){
284 var q=(filter||"").trim().toLowerCase(), rows=document.getElementById('rows');
285 clear(rows);
286 var list = q ? SUM.filter(function(b){ return String(b.height)===q || b.hash.indexOf(q)===0; }) : SUM.slice(0,SHOWN);
287 if(!list.length){
288 var empty = E('tr');
289 var td = E('td', 'mut', 'no match');
290 td.colSpan = 6;
291 append(empty, td); append(rows, empty);
292 }
293 list.forEach(function(b){
294 var tr = row([String(b.height), ts(b.time), String(b.nTx), b.size+' B', btf(b.cbValue), ''], 'blk');
295 tr.dataset.h = b.height;
296 clear(tr.children[5]);
297 append(tr.children[5], code(b.cbAddr || '-'));
298 tr.onclick = function(){ openBlock(tr.dataset.h); };
299 append(rows, tr);
300 });
301 document.getElementById('more').textContent = (!q && SUM.length>SHOWN) ? ("showing latest "+SHOWN+" of "+SUM.length+" blocks; search to find any") : "";
302}
303function kv(parent, key, value, isCode){
304 append(parent, E('div', '', key));
305 append(parent, isCode ? append(E('div'), code(value)) : E('div', '', value));
306}
307function ioSide(title, items, render){
308 var side = E('div');
309 append(side, E('div', 'mut', title));
310 items.forEach(function(item){ append(side, render(item)); });
311 return side;
312}
313function openBlock(h){
314 fetch('block/'+h+'.json').then(function(r){return r.json();}).then(function(b){
315 var detail = document.getElementById('detail');
316 clear(detail);
317 append(detail, E('h2', '', 'Block '+b.height));
318 var card = E('div', 'card'), grid = E('div', 'kv');
319 kv(grid, 'Height', String(b.height));
320 kv(grid, 'Hash', b.hash, true);
321 var prev = E('div');
322 if(b.height > 0){
323 var btn = E('button', 'link');
324 append(btn, code(short(b.prev)));
325 btn.onclick = function(){ openBlock(b.height - 1); };
326 append(prev, btn);
327 } else {
328 append(prev, code(short(b.prev)));
329 }
330 append(grid, E('div', '', 'Previous'), prev);
331 kv(grid, 'Merkle root', b.merkleRoot, true);
332 kv(grid, 'Time (UTC)', ts(b.time));
333 kv(grid, 'Bits / Nonce', b.bits.toString(16)+' / '+b.nonce);
334 kv(grid, 'Size / Txs', b.size+' B / '+b.txs.length);
335 append(card, grid); append(detail, card);
336 b.txs.forEach(function(t, i){
337 var tx = E('div', 'tx');
338 var head = E('div');
339 append(head, code(t.txid));
340 if(i === 0) append(head, E('span', 'pill', 'coinbase'));
341 var io = E('div', 'io');
342 append(io, ioSide('Inputs', t.vin, function(v){
343 var d = E('div');
344 if(v.coinbase) append(d, E('span', 'pill', 'coinbase'));
345 else append(d, code(short(v.prev)+':'+v.vout));
346 return d;
347 }));
348 append(io, ioSide('Outputs', t.vout, function(o){
349 var d = E('div');
350 var to = o.address ? o.address : (o.pubkey ? 'pubkey '+o.pubkey.slice(0,16)+'...' : o.type);
351 append(d, document.createTextNode(btf(o.value)+' -> '), code(to));
352 return d;
353 }));
354 append(tx, head, io); append(detail, tx);
355 });
356 document.getElementById('listview').hidden = true;
357 detail.hidden = false;
358 document.getElementById('home').hidden = false;
359 window.scrollTo(0,0);
360 });
361}
362document.getElementById('home').onclick=function(){
363 document.getElementById('detail').hidden = true;
364 document.getElementById('listview').hidden = false;
365 this.hidden = true;
366};
367document.getElementById('q').oninput=function(){ renderList(this.value); };
368fetch('blocks.json').then(function(r){return r.json();}).then(function(d){
369 SUM=d.blocks; document.getElementById('tip').textContent='tip height '+d.tipHeight+' - '+d.count+' blocks';
370 renderList('');
371});
372"""
373
374
375if __name__ == "__main__":
376 raise SystemExit(main(sys.argv[1:]))