upleb.uk

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

summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorBitflash <admin@bitflash.network>2026-08-10 11:50:10 -0300
committerBitflash <admin@bitflash.network>2026-08-10 11:50:10 -0300
commitd2679e2c09c17e4af50535c13324ba2ddb2eb4ad (patch)
tree61a607f326fd84f02b3b9e957448552c2b41655f /scripts
parent64317cc5a3416d77c9171351c367ab8c24c4dff1 (diff)
tools: static block explorer generator (v0)
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/build-explorer.py243
1 files changed, 243 insertions, 0 deletions
diff --git a/scripts/build-explorer.py b/scripts/build-explorer.py
new file mode 100755
index 0000000..7849e8a
--- /dev/null
+++ b/scripts/build-explorer.py
@@ -0,0 +1,243 @@
1#!/usr/bin/env python3
2#
3# Build a static Bitflash block explorer (v0) from a node's blk0001.dat.
4# Emits into <out-dir>:
5# blocks.json summary of every main-chain block (newest first)
6# block/<height>.json full detail per block (header + all txs)
7# index.html self-contained viewer (vanilla JS, same-origin fetch)
8#
9# Scope (v0, deliberately): block list, block detail, and coinbase. No mempool,
10# no per-address history. On this chain the coinbase payout key rotates every
11# block, so an address index would be nearly useless; block-level transparency is
12# what lets miners and users confirm the chain is honest.
13#
14# Usage: build-explorer.py <blk0001.dat> <out-dir>
15
16import sys, os, json, hashlib, struct
17
18MAGIC = bytes([0xbf, 0x20, 0x5c, 0xfd])
19ADDRESS_VERSION = 25 # Bitflash addresses start with "B"
20
21def sha256d(b): return hashlib.sha256(hashlib.sha256(b).digest()).digest()
22
23B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
24def b58check(payload):
25 v = bytes([ADDRESS_VERSION]) + payload
26 v += sha256d(v)[:4]
27 n = int.from_bytes(v, 'big')
28 s = ""
29 while n > 0:
30 n, r = divmod(n, 58); s = B58[r] + s
31 for byte in v:
32 if byte == 0: s = "1" + s
33 else: break
34 return s
35
36def hash160(b):
37 return hashlib.new('ripemd160', hashlib.sha256(b).digest()).digest()
38
39class R:
40 def __init__(s, b, o=0): s.b = b; s.o = o
41 def u(s, n): v = s.b[s.o:s.o+n]; s.o += n; return v
42 def i32(s): return struct.unpack('<i', s.u(4))[0]
43 def u32(s): return struct.unpack('<I', s.u(4))[0]
44 def i64(s): return struct.unpack('<q', s.u(8))[0]
45 def cs(s):
46 c = s.b[s.o]; s.o += 1
47 if c < 253: return c
48 if c == 253: return struct.unpack('<H', s.u(2))[0]
49 if c == 254: return struct.unpack('<I', s.u(4))[0]
50 return struct.unpack('<Q', s.u(8))[0]
51 def script(s): n = s.cs(); return s.u(n)
52
53def spk_info(spk):
54 # P2PKH: OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG
55 if len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xa9 and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xac:
56 return {"type": "p2pkh", "address": b58check(spk[3:23])}
57 # P2PK: <pubkey> OP_CHECKSIG (65- or 33-byte key)
58 if len(spk) >= 35 and spk[-1] == 0xac:
59 if spk[0] == 0x41 and len(spk) == 67:
60 pk = spk[1:66]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))}
61 if spk[0] == 0x21 and len(spk) == 35:
62 pk = spk[1:34]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))}
63 return {"type": "other", "hex": spk.hex()[:80]}
64
65def parse_tx(r):
66 start = r.o
67 r.i32()
68 nin = r.cs(); vin = []
69 for _ in range(nin):
70 ph = r.u(32); idx = r.u32(); r.script(); r.u32()
71 coinbase = (ph == b'\x00' * 32 and idx == 0xffffffff)
72 vin.append({"coinbase": coinbase, "prev": (None if coinbase else ph[::-1].hex()), "vout": (None if coinbase else idx)})
73 nout = r.cs(); vout = []
74 for _ in range(nout):
75 val = r.i64(); spk = r.script(); vout.append(dict({"value": val}, **spk_info(spk)))
76 r.u32()
77 raw = r.b[start:r.o]
78 return {"txid": sha256d(raw)[::-1].hex(), "vin": vin, "vout": vout, "size": len(raw)}
79
80def main():
81 if len(sys.argv) != 3:
82 print("usage: build-explorer.py <blk0001.dat> <out-dir>", file=sys.stderr); sys.exit(2)
83 blkpath, out = sys.argv[1], sys.argv[2]
84 data = open(blkpath, 'rb').read()
85 blocks = {}; o = 0; n = len(data)
86 while o + 8 <= n:
87 if data[o:o+4] != MAGIC: o += 1; continue
88 o += 4; size = struct.unpack('<I', data[o:o+4])[0]; o += 4
89 blk = data[o:o+size]; o += size
90 if len(blk) < 80: continue
91 h = sha256d(blk[:80])[::-1].hex()
92 r = R(blk, 0)
93 ver = r.i32(); prev = r.u(32)[::-1].hex(); merk = r.u(32)[::-1].hex()
94 t = r.u32(); bits = r.u32(); nonce = r.u32()
95 ntx = r.cs(); txs = []
96 try:
97 for _ in range(ntx): txs.append(parse_tx(r))
98 except Exception:
99 continue
100 blocks[h] = {"hash": h, "prev": prev, "merkleRoot": merk, "version": ver,
101 "time": t, "bits": bits, "nonce": nonce, "size": size, "txs": txs}
102 if not blocks:
103 print("no blocks parsed", file=sys.stderr); sys.exit(1)
104
105 hc = {}
106 def height(h):
107 chain = []; cur = h
108 while cur in blocks and cur not in hc:
109 chain.append(cur); cur = blocks[cur]["prev"]
110 base = hc.get(cur, 0)
111 for hh in reversed(chain): base += 1; hc[hh] = base
112 return hc[h]
113 for h in blocks: height(h)
114 tip = max(blocks, key=lambda h: hc[h])
115 chain = []; cur = tip
116 while cur in blocks: chain.append(cur); cur = blocks[cur]["prev"]
117 chain.reverse()
118
119 os.makedirs(os.path.join(out, "block"), exist_ok=True)
120 summaries = []
121 for h in chain:
122 b = blocks[h]; H = hc[h]
123 cb = b["txs"][0]["vout"][0] if b["txs"] and b["txs"][0]["vout"] else {}
124 summaries.append({"height": H, "hash": h, "time": b["time"], "nTx": len(b["txs"]),
125 "size": b["size"], "cbValue": cb.get("value", 0), "cbAddr": cb.get("address", "")})
126 json.dump(dict({"height": H}, **b), open(os.path.join(out, "block", "%d.json" % H), "w"), separators=(',', ':'))
127 summaries.sort(key=lambda x: -x["height"])
128 json.dump({"network": "bitflash", "tipHeight": hc[tip], "count": len(chain), "blocks": summaries},
129 open(os.path.join(out, "blocks.json"), "w"), separators=(',', ':'))
130 open(os.path.join(out, "index.html"), "w").write(INDEX_HTML)
131 print("explorer built: %d main-chain blocks, tip %d, into %s" % (len(chain), hc[tip], out))
132
133
134INDEX_HTML = r"""<!doctype html>
135<html lang="en">
136<head>
137<meta charset="utf-8">
138<meta name="viewport" content="width=device-width, initial-scale=1">
139<title>Bitflash Block Explorer</title>
140<style>
141 :root { color-scheme: light dark; --bg:#fff; --fg:#111; --mut:#666; --line:#e2e2e2; --card:#fafafa; --acc:#2b6cb0; }
142 @media (prefers-color-scheme: dark){ :root{ --bg:#0e0f11; --fg:#e6e6e6; --mut:#9aa0a6; --line:#26282c; --card:#16181b; --acc:#63b3ed; } }
143 * { box-sizing: border-box; }
144 body { font: 15px/1.5 system-ui, sans-serif; margin:0; background:var(--bg); color:var(--fg); }
145 header { padding:20px 24px; border-bottom:1px solid var(--line); display:flex; gap:16px; align-items:baseline; flex-wrap:wrap; }
146 h1 { font-size:18px; margin:0; }
147 .mut { color:var(--mut); }
148 .wrap { padding:16px 24px; max-width:1100px; margin:0 auto; }
149 input { width:100%; max-width:420px; padding:9px 12px; border:1px solid var(--line); border-radius:8px; background:var(--card); color:var(--fg); font-size:14px; }
150 table { width:100%; border-collapse:collapse; margin-top:12px; }
151 th,td { text-align:left; padding:9px 10px; border-bottom:1px solid var(--line); font-variant-numeric:tabular-nums; }
152 th { font-size:12px; text-transform:uppercase; letter-spacing:.05em; color:var(--mut); }
153 tr.blk { cursor:pointer; }
154 tr.blk:hover td { background:var(--card); }
155 code { font-size:12px; word-break:break-all; }
156 .scroll { overflow-x:auto; }
157 a { color:var(--acc); text-decoration:none; }
158 .card { border:1px solid var(--line); border-radius:10px; background:var(--card); padding:14px 16px; margin-top:12px; }
159 .kv { display:grid; grid-template-columns:150px 1fr; gap:4px 12px; font-size:13px; }
160 .kv div:nth-child(odd){ color:var(--mut); }
161 .tx { border-top:1px solid var(--line); padding:10px 0; }
162 .io { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
163 @media (max-width:640px){ .io{ grid-template-columns:1fr; } .kv{ grid-template-columns:110px 1fr; } }
164 .pill { display:inline-block; font-size:11px; padding:1px 7px; border-radius:99px; border:1px solid var(--line); color:var(--mut); }
165 button.link { background:none; border:none; color:var(--acc); cursor:pointer; font:inherit; padding:0; }
166 .note { color:var(--mut); font-size:13px; max-width:820px; }
167</style>
168</head>
169<body>
170<header>
171 <h1>Bitflash Block Explorer</h1>
172 <span class="mut" id="tip"></span>
173 <span style="flex:1"></span>
174 <button class="link" id="home" style="display:none">back to blocks</button>
175</header>
176<div class="wrap">
177 <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. On Bitflash the coinbase payout key rotates every block, so an address index would say almost nothing; what matters is that every block and every coinbase is here to check.</p>
178 <div id="listview">
179 <input id="q" placeholder="Search by height or block hash">
180 <div class="scroll"><table>
181 <thead><tr><th>Height</th><th>Time (UTC)</th><th>Txs</th><th>Size</th><th>Coinbase</th><th>To</th></tr></thead>
182 <tbody id="rows"></tbody>
183 </table></div>
184 <p class="mut" id="more"></p>
185 </div>
186 <div id="detail" style="display:none"></div>
187</div>
188<script>
189var SUM = [], BYH = {}, SHOWN = 200;
190function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; }
191function ts(t){ return new Date(t*1000).toISOString().replace('T',' ').replace('.000Z',' UTC'); }
192function esc(s){ return String(s).replace(/[&<>]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;'}[c];}); }
193function short(h){ return h ? h.slice(0,10)+"…"+h.slice(-6) : "-"; }
194
195function renderList(filter){
196 var q=(filter||"").trim().toLowerCase(), rows=document.getElementById('rows'), out="";
197 var list = q ? SUM.filter(function(b){ return String(b.height)===q || b.hash.indexOf(q)===0; }) : SUM.slice(0,SHOWN);
198 for(var i=0;i<list.length;i++){ var b=list[i];
199 out += '<tr class="blk" data-h="'+b.height+'"><td>'+b.height+'</td><td>'+ts(b.time)+'</td><td>'+b.nTx+'</td><td>'+b.size+' B</td><td>'+btf(b.cbValue)+'</td><td><code>'+(b.cbAddr?esc(b.cbAddr):'-')+'</code></td></tr>';
200 }
201 rows.innerHTML = out || '<tr><td colspan="6" class="mut">no match</td></tr>';
202 document.getElementById('more').textContent = (!q && SUM.length>SHOWN) ? ("showing latest "+SHOWN+" of "+SUM.length+" blocks; search to find any") : "";
203 Array.prototype.forEach.call(rows.querySelectorAll('tr.blk'), function(tr){ tr.onclick=function(){ openBlock(tr.getAttribute('data-h')); }; });
204}
205function ioSide(title, items, html){
206 var s='<div><div class="mut">'+title+'</div>'; for(var i=0;i<items.length;i++){ s+=html(items[i]); } return s+'</div>';
207}
208function openBlock(h){
209 fetch('block/'+h+'.json').then(function(r){return r.json();}).then(function(b){
210 var s='<div class="card"><div class="kv">'+
211 '<div>Height</div><div>'+b.height+'</div>'+
212 '<div>Hash</div><div><code>'+esc(b.hash)+'</code></div>'+
213 '<div>Previous</div><div><button class="link" onclick="openBlock('+(b.height-1)+')"><code>'+esc(short(b.prev))+'</code></button></div>'+
214 '<div>Merkle root</div><div><code>'+esc(b.merkleRoot)+'</code></div>'+
215 '<div>Time (UTC)</div><div>'+ts(b.time)+'</div>'+
216 '<div>Bits / Nonce</div><div>'+b.bits.toString(16)+' / '+b.nonce+'</div>'+
217 '<div>Size / Txs</div><div>'+b.size+' B / '+b.txs.length+'</div>'+
218 '</div></div>';
219 for(var i=0;i<b.txs.length;i++){ var t=b.txs[i];
220 s+='<div class="tx"><div><code>'+esc(t.txid)+'</code> '+(i===0?'<span class="pill">coinbase</span>':'')+'</div><div class="io">';
221 s+=ioSide('Inputs', t.vin, function(v){ return '<div>'+(v.coinbase?'<span class="pill">coinbase</span>':'<code>'+esc(short(v.prev))+':'+v.vout+'</code>')+'</div>'; });
222 s+=ioSide('Outputs', t.vout, function(o){ var to=o.address?esc(o.address):(o.pubkey?'pubkey '+esc(o.pubkey.slice(0,16))+'…':esc(o.type)); return '<div>'+btf(o.value)+' → <code>'+to+'</code></div>'; });
223 s+='</div></div>';
224 }
225 document.getElementById('listview').style.display='none';
226 var d=document.getElementById('detail'); d.style.display='block'; d.innerHTML='<h2 style="font-size:16px">Block '+b.height+'</h2>'+s;
227 document.getElementById('home').style.display='inline';
228 window.scrollTo(0,0);
229 });
230}
231document.getElementById('home').onclick=function(){ document.getElementById('detail').style.display='none'; document.getElementById('listview').style.display='block'; this.style.display='none'; };
232document.getElementById('q').oninput=function(){ renderList(this.value); };
233fetch('blocks.json').then(function(r){return r.json();}).then(function(d){
234 SUM=d.blocks; document.getElementById('tip').textContent='tip height '+d.tipHeight+' · '+d.count+' blocks';
235 renderList('');
236});
237</script>
238</body>
239</html>
240"""
241
242if __name__ == "__main__":
243 main()