#!/usr/bin/env python3 """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 += chain.sha256d(v)[:4] n = int.from_bytes(v, "big") s = "" while n > 0: n, r = divmod(n, 58) s = B58[r] + s for byte in v: if byte == 0: s = "1" + s else: break return s def hash160(b): return hashlib.new("ripemd160", hashlib.sha256(b).digest()).digest() def spk_info(script_hex): try: spk = bytes.fromhex(script_hex) except ValueError: return {"type": "malformed", "hex": script_hex[:80]} 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])} 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))} 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": script_hex[:80]} def explorer_tx(tx): vin = [] for txin in tx["vin"]: coinbase = ( txin["prevout_hash"] == "0" * 64 and txin["prevout_n"] == 0xFFFFFFFF ) vin.append({ "coinbase": coinbase, "prev": None if coinbase else txin["prevout_hash"], "vout": None if coinbase else txin["prevout_n"], }) vout = [] for out in tx["vout"]: vout.append(dict({"value": out["value_satoshis"]}, **spk_info(out["script_pub_key_hex"]))) return {"txid": tx["txid"], "vin": vin, "vout": vout, "size": len(chain.ser_tx(tx))} def parse_args(argv): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--datadir", help="Bitflash data directory containing blk*.dat") ap.add_argument("--max-blocks", type=int, default=0, help="number of blocks to scan; 0 means all block files") ap.add_argument("paths", nargs="+", help="with --datadir: ; otherwise: ... ") args = ap.parse_args(argv) if args.datadir: if len(args.paths) != 1: raise SystemExit("usage with --datadir: build-explorer.py --datadir DATADIR ") 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 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"]) (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""" Bitflash Block Explorer

Bitflash Block Explorer

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

HeightTime (UTC)TxsSizeCoinbaseTo

""" if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))