From 1839347459759fb3b23cc747ec4368b724fee008 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Tue, 11 Aug 2026 14:13:26 -0300 Subject: tools: harden chain audit scripts --- scripts/build-explorer.py | 382 ++++++++++++++++++++++++++++------------------ 1 file changed, 236 insertions(+), 146 deletions(-) (limited to 'scripts/build-explorer.py') diff --git a/scripts/build-explorer.py b/scripts/build-explorer.py index 7849e8a..0d86b3c 100755 --- a/scripts/build-explorer.py +++ b/scripts/build-explorer.py @@ -1,134 +1,161 @@ #!/usr/bin/env python3 -# -# Build a static Bitflash block explorer (v0) from a node's blk0001.dat. -# Emits into : -# blocks.json summary of every main-chain block (newest first) -# block/.json full detail per block (header + all txs) -# index.html self-contained viewer (vanilla JS, same-origin fetch) -# -# Scope (v0, deliberately): block list, block detail, and coinbase. No mempool, -# no per-address history. On this chain the coinbase payout key rotates every -# block, so an address index would be nearly useless; block-level transparency is -# what lets miners and users confirm the chain is honest. -# -# Usage: build-explorer.py - -import sys, os, json, hashlib, struct - -MAGIC = bytes([0xbf, 0x20, 0x5c, 0xfd]) -ADDRESS_VERSION = 25 # Bitflash addresses start with "B" - -def sha256d(b): return hashlib.sha256(hashlib.sha256(b).digest()).digest() +"""Build a static Bitflash block explorer from local blk*.dat files.""" +import argparse +import hashlib +import json +import sys +from pathlib import Path + +import bitflash_chain as chain + + +ADDRESS_VERSION = 25 B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + def b58check(payload): v = bytes([ADDRESS_VERSION]) + payload - v += sha256d(v)[:4] - n = int.from_bytes(v, 'big') + v += chain.sha256d(v)[:4] + n = int.from_bytes(v, "big") s = "" while n > 0: - n, r = divmod(n, 58); s = B58[r] + s + n, r = divmod(n, 58) + s = B58[r] + s for byte in v: - if byte == 0: s = "1" + s - else: break + if byte == 0: + s = "1" + s + else: + break return s + def hash160(b): - return hashlib.new('ripemd160', hashlib.sha256(b).digest()).digest() - -class R: - def __init__(s, b, o=0): s.b = b; s.o = o - def u(s, n): v = s.b[s.o:s.o+n]; s.o += n; return v - def i32(s): return struct.unpack(' OP_EQUALVERIFY OP_CHECKSIG - if len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xa9 and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xac: + return hashlib.new("ripemd160", hashlib.sha256(b).digest()).digest() + + +def spk_info(script_hex): + spk = bytes.fromhex(script_hex) + if ( + len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xA9 + and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xAC + ): return {"type": "p2pkh", "address": b58check(spk[3:23])} - # P2PK: OP_CHECKSIG (65- or 33-byte key) - if len(spk) >= 35 and spk[-1] == 0xac: + if len(spk) >= 35 and spk[-1] == 0xAC: if spk[0] == 0x41 and len(spk) == 67: - pk = spk[1:66]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} + pk = spk[1:66] + return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} if spk[0] == 0x21 and len(spk) == 35: - pk = spk[1:34]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} - return {"type": "other", "hex": spk.hex()[:80]} - -def parse_tx(r): - start = r.o - r.i32() - nin = r.cs(); vin = [] - for _ in range(nin): - ph = r.u(32); idx = r.u32(); r.script(); r.u32() - coinbase = (ph == b'\x00' * 32 and idx == 0xffffffff) - vin.append({"coinbase": coinbase, "prev": (None if coinbase else ph[::-1].hex()), "vout": (None if coinbase else idx)}) - nout = r.cs(); vout = [] - for _ in range(nout): - val = r.i64(); spk = r.script(); vout.append(dict({"value": val}, **spk_info(spk))) - r.u32() - raw = r.b[start:r.o] - return {"txid": sha256d(raw)[::-1].hex(), "vin": vin, "vout": vout, "size": len(raw)} - -def main(): - if len(sys.argv) != 3: - print("usage: build-explorer.py ", file=sys.stderr); sys.exit(2) - blkpath, out = sys.argv[1], sys.argv[2] - data = open(blkpath, 'rb').read() - blocks = {}; o = 0; n = len(data) - while o + 8 <= n: - if data[o:o+4] != MAGIC: o += 1; continue - o += 4; size = struct.unpack('") + args.block_files = None + args.out_dir = args.paths[0] + else: + if len(args.paths) < 2: + raise SystemExit("usage: build-explorer.py ... ") + args.block_files = args.paths[:-1] + args.out_dir = args.paths[-1] + return args + + +def load_blocks(args): + if args.datadir: + return chain.read_blocks(args.datadir, args.max_blocks) + return chain.read_blocks_from_files(args.block_files, args.max_blocks) + + +def build_explorer(raw_blocks, out_dir): + try: + blocks = chain.select_main_chain(raw_blocks) + except chain.ParseError as e: + raise SystemExit(str(e)) + out = Path(out_dir) + (out / "block").mkdir(parents=True, exist_ok=True) + summaries = [] - for h in chain: - b = blocks[h]; H = hc[h] - cb = b["txs"][0]["vout"][0] if b["txs"] and b["txs"][0]["vout"] else {} - summaries.append({"height": H, "hash": h, "time": b["time"], "nTx": len(b["txs"]), - "size": b["size"], "cbValue": cb.get("value", 0), "cbAddr": cb.get("address", "")}) - json.dump(dict({"height": H}, **b), open(os.path.join(out, "block", "%d.json" % H), "w"), separators=(',', ':')) + for height, block in enumerate(blocks): + txs = [explorer_tx(tx) for tx in block["transactions"]] + detail = { + "height": height, + "hash": block["hash"], + "prev": block["previous_hash"], + "merkleRoot": block["merkle_root"], + "version": block["version"], + "time": block["time"], + "bits": int(block["bits"], 16), + "nonce": block["nonce"], + "size": block["size"], + "txs": txs, + } + cb = txs[0]["vout"][0] if txs and txs[0]["vout"] else {} + summaries.append({ + "height": height, + "hash": block["hash"], + "time": block["time"], + "nTx": len(txs), + "size": block["size"], + "cbValue": cb.get("value", 0), + "cbAddr": cb.get("address", ""), + }) + (out / "block" / ("%d.json" % height)).write_text( + json.dumps(detail, sort_keys=True, separators=(",", ":")) + "\n", + encoding="ascii", + ) + summaries.sort(key=lambda x: -x["height"]) - json.dump({"network": "bitflash", "tipHeight": hc[tip], "count": len(chain), "blocks": summaries}, - open(os.path.join(out, "blocks.json"), "w"), separators=(',', ':')) - open(os.path.join(out, "index.html"), "w").write(INDEX_HTML) - print("explorer built: %d main-chain blocks, tip %d, into %s" % (len(chain), hc[tip], out)) + (out / "blocks.json").write_text( + json.dumps({ + "network": "bitflash", + "tipHeight": len(blocks) - 1, + "count": len(blocks), + "blocks": summaries, + }, sort_keys=True, separators=(",", ":")) + "\n", + encoding="ascii", + ) + (out / "index.html").write_text(INDEX_HTML, encoding="utf-8") + return len(blocks), len(blocks) - 1 + + +def main(argv): + args = parse_args(argv) + raw_blocks = load_blocks(args) + if not raw_blocks: + raise SystemExit("no Bitflash blocks parsed") + count, tip = build_explorer(raw_blocks, args.out_dir) + print("explorer built: %d main-chain blocks, tip %d, into %s" % (count, tip, args.out_dir)) + return 0 INDEX_HTML = r""" @@ -144,6 +171,7 @@ INDEX_HTML = r""" body { font: 15px/1.5 system-ui, sans-serif; margin:0; background:var(--bg); color:var(--fg); } header { padding:20px 24px; border-bottom:1px solid var(--line); display:flex; gap:16px; align-items:baseline; flex-wrap:wrap; } h1 { font-size:18px; margin:0; } + h2 { font-size:16px; } .mut { color:var(--mut); } .wrap { padding:16px 24px; max-width:1100px; margin:0 auto; } 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; } @@ -154,7 +182,6 @@ INDEX_HTML = r""" tr.blk:hover td { background:var(--card); } code { font-size:12px; word-break:break-all; } .scroll { overflow-x:auto; } - a { color:var(--acc); text-decoration:none; } .card { border:1px solid var(--line); border-radius:10px; background:var(--card); padding:14px 16px; margin-top:12px; } .kv { display:grid; grid-template-columns:150px 1fr; gap:4px 12px; font-size:13px; } .kv div:nth-child(odd){ color:var(--mut); } @@ -174,7 +201,7 @@ INDEX_HTML = r"""
-

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.

+

Read-only view of the main chain: blocks, their transactions, and the coinbase payout. There is no mempool and no per-address history here.

@@ -186,52 +213,114 @@ INDEX_HTML = r""" @@ -239,5 +328,6 @@ fetch('blocks.json').then(function(r){return r.json();}).then(function(d){ """ + if __name__ == "__main__": - main() + raise SystemExit(main(sys.argv[1:])) -- cgit v1.2.3