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:
authorNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-11 14:13:26 -0300
committerNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-11 14:13:26 -0300
commit1839347459759fb3b23cc747ec4368b724fee008 (patch)
treee5f6f7e13b6dd9eb260d7c75bafa19d873435c5c /scripts
parenta83fae37099cb102beb5a4c174a4abe6ef3f24a4 (diff)
tools: harden chain audit scripts
Diffstat (limited to 'scripts')
-rw-r--r--scripts/bitflash_chain.py453
-rwxr-xr-xscripts/build-explorer.py382
-rw-r--r--scripts/prove-utxo.py43
-rw-r--r--scripts/verify-fair-launch.py346
-rw-r--r--scripts/verify-utxo-proof.py23
-rw-r--r--scripts/verify-utxo-set.py375
6 files changed, 758 insertions, 864 deletions
diff --git a/scripts/bitflash_chain.py b/scripts/bitflash_chain.py
new file mode 100644
index 0000000..f3688f1
--- /dev/null
+++ b/scripts/bitflash_chain.py
@@ -0,0 +1,453 @@
1#!/usr/bin/env python3
2"""Shared Bitflash block/chain parser for deterministic audit tools."""
3
4import datetime as _dt
5import hashlib
6import json
7import os
8import struct
9from pathlib import Path
10
11
12MAGIC = bytes([0xBF, 0x20, 0x5C, 0xFD])
13MAX_BLOCK_PAYLOAD = 32 * 1024 * 1024
14COIN = 100_000_000
15GENESIS_HASH = "5bd7cb255d814e48cebcdfb72da4dc87b34bd774227f8ceb546c5640f4bdc169"
16GENESIS_MERKLE = "1a45b4482532abb29b10e234d3f13132230525a339ecea91658ffa675a5b1325"
17GENESIS_TIME = 1_753_315_200
18GENESIS_BITS = 0x1F0FFFFF
19GENESIS_NONCE = 3141
20GENESIS_TEXT = "Bitflash 24/Jul/2026 Fair launch: one CPU one vote, no premine"
21
22
23class ParseError(Exception):
24 pass
25
26
27class Reader:
28 def __init__(self, data):
29 self.data = data
30 self.pos = 0
31
32 def take(self, n):
33 if self.pos + n > len(self.data):
34 raise ParseError("unexpected end of data")
35 out = self.data[self.pos:self.pos + n]
36 self.pos += n
37 return out
38
39 def u8(self):
40 return self.take(1)[0]
41
42 def u32(self):
43 return struct.unpack("<I", self.take(4))[0]
44
45 def i32(self):
46 return struct.unpack("<i", self.take(4))[0]
47
48 def i64(self):
49 return struct.unpack("<q", self.take(8))[0]
50
51
52def sha256(data):
53 return hashlib.sha256(data).digest()
54
55
56def sha256d(data):
57 return hashlib.sha256(hashlib.sha256(data).digest()).digest()
58
59
60def hash_hex(serialized):
61 return sha256d(serialized)[::-1].hex()
62
63
64def compact_size(r):
65 first = r.u8()
66 if first < 253:
67 return first
68 if first == 253:
69 return struct.unpack("<H", r.take(2))[0]
70 if first == 254:
71 return r.u32()
72 return struct.unpack("<Q", r.take(8))[0]
73
74
75def read_varbytes(r):
76 return r.take(compact_size(r))
77
78
79def ser_compact_size(n):
80 if n < 253:
81 return bytes([n])
82 if n <= 0xFFFF:
83 return b"\xfd" + struct.pack("<H", n)
84 if n <= 0xFFFFFFFF:
85 return b"\xfe" + struct.pack("<I", n)
86 return b"\xff" + struct.pack("<Q", n)
87
88
89def ser_varbytes(b):
90 return ser_compact_size(len(b)) + b
91
92
93def ser_tx(tx):
94 out = bytearray()
95 out += struct.pack("<i", tx["version"])
96 out += ser_compact_size(len(tx["vin"]))
97 for txin in tx["vin"]:
98 out += bytes.fromhex(txin["prevout_hash"])[::-1]
99 out += struct.pack("<I", txin["prevout_n"])
100 out += ser_varbytes(bytes.fromhex(txin["script_sig_hex"]))
101 out += struct.pack("<I", txin["sequence"])
102 out += ser_compact_size(len(tx["vout"]))
103 for txout in tx["vout"]:
104 out += struct.pack("<q", txout["value_satoshis"])
105 out += ser_varbytes(bytes.fromhex(txout["script_pub_key_hex"]))
106 out += struct.pack("<I", tx["lock_time"])
107 return bytes(out)
108
109
110def parse_tx(r):
111 start = r.pos
112 tx = {"version": r.i32(), "vin": [], "vout": [], "lock_time": 0}
113 for _ in range(compact_size(r)):
114 tx["vin"].append({
115 "prevout_hash": r.take(32)[::-1].hex(),
116 "prevout_n": r.u32(),
117 "script_sig_hex": read_varbytes(r).hex(),
118 "sequence": r.u32(),
119 })
120 for _ in range(compact_size(r)):
121 value = r.i64()
122 tx["vout"].append({
123 "value_satoshis": value,
124 "value_btf": format_money(value),
125 "script_pub_key_hex": read_varbytes(r).hex(),
126 })
127 tx["lock_time"] = r.u32()
128 raw = r.data[start:r.pos]
129 tx["txid"] = hash_hex(raw)
130 return tx
131
132
133def merkle_root(txids):
134 if not txids:
135 return "0" * 64
136 layer = [bytes.fromhex(x)[::-1] for x in txids]
137 while len(layer) > 1:
138 nxt = []
139 for i in range(0, len(layer), 2):
140 left = layer[i]
141 right = layer[i + 1] if i + 1 < len(layer) else left
142 nxt.append(sha256d(left + right))
143 layer = nxt
144 return layer[0][::-1].hex()
145
146
147def parse_block(payload, file_name="", offset=0):
148 r = Reader(payload)
149 header = r.take(80)
150 version, = struct.unpack("<i", header[0:4])
151 prev = header[4:36][::-1].hex()
152 merkle = header[36:68][::-1].hex()
153 ntime, bits, nonce = struct.unpack("<III", header[68:80])
154 txs = [parse_tx(r) for _ in range(compact_size(r))]
155 if r.pos != len(payload):
156 raise ParseError("trailing bytes inside block")
157 txids = [tx["txid"] for tx in txs]
158 return {
159 "hash": hash_hex(header),
160 "version": version,
161 "previous_hash": prev,
162 "merkle_root": merkle,
163 "computed_merkle_root": merkle_root(txids),
164 "time": ntime,
165 "time_utc": utc(ntime),
166 "bits": "0x%08x" % bits,
167 "nonce": nonce,
168 "tx_count": len(txs),
169 "transactions": txs,
170 "source_file": file_name,
171 "source_offset": offset,
172 "size": len(payload),
173 }
174
175
176def iter_block_files(datadir):
177 for path in sorted(Path(datadir).glob("blk*.dat")):
178 yield path
179
180
181def iter_blocks_from_file(path):
182 path = Path(path)
183 with path.open("rb") as f:
184 window = f.read(4)
185 offset = 0
186 while len(window) == 4:
187 if window != MAGIC:
188 nxt = f.read(1)
189 if not nxt:
190 break
191 window = window[1:] + nxt
192 offset += 1
193 continue
194
195 size_bytes = f.read(4)
196 if len(size_bytes) != 4:
197 break
198 size = struct.unpack("<I", size_bytes)[0]
199 if size == 0 or size > MAX_BLOCK_PAYLOAD:
200 offset += 1
201 f.seek(offset)
202 window = f.read(4)
203 continue
204 payload_offset = offset + 8
205 payload = f.read(size)
206 if len(payload) != size:
207 break
208 try:
209 yield parse_block(payload, path.name, payload_offset)
210 except ParseError:
211 offset += 1
212 f.seek(offset)
213 window = f.read(4)
214 continue
215
216 offset = f.tell()
217 window = f.read(4)
218
219
220def read_blocks_from_files(paths, max_blocks=0):
221 blocks = []
222 for path in paths:
223 for block in iter_blocks_from_file(path):
224 blocks.append(block)
225 if max_blocks and len(blocks) >= max_blocks:
226 return blocks
227 return blocks
228
229
230def read_blocks(datadir, max_blocks=0):
231 return read_blocks_from_files(iter_block_files(datadir), max_blocks)
232
233
234def select_main_chain(blocks):
235 by_hash = {}
236 for block in blocks:
237 by_hash.setdefault(block["hash"], block)
238 if GENESIS_HASH not in by_hash:
239 raise ParseError("genesis block not found in block files")
240
241 children = {}
242 for block in by_hash.values():
243 children.setdefault(block["previous_hash"], []).append(block)
244 for v in children.values():
245 v.sort(key=lambda b: (b["hash"], b["source_file"], b["source_offset"]))
246
247 best = []
248 stack = [(by_hash[GENESIS_HASH], [by_hash[GENESIS_HASH]])]
249 while stack:
250 block, path = stack.pop()
251 if len(path) > len(best) or (len(path) == len(best) and block["hash"] < best[-1]["hash"]):
252 best = path
253 for child in children.get(block["hash"], []):
254 stack.append((child, path + [child]))
255 return best
256
257
258def is_coinbase(tx):
259 return (
260 len(tx["vin"]) == 1
261 and tx["vin"][0]["prevout_hash"] == "0" * 64
262 and tx["vin"][0]["prevout_n"] == 0xFFFFFFFF
263 )
264
265
266def utxo_leaf_hash(outpoint, utxo):
267 payload = "|".join([
268 "BTFUTXO1",
269 outpoint,
270 str(utxo["value_satoshis"]),
271 utxo["script_pub_key_hex"],
272 str(utxo["created_height"]),
273 utxo["created_txid"],
274 ]).encode("ascii")
275 return sha256(payload)
276
277
278def utxo_root(utxos):
279 leaves = [utxo_leaf_hash(k, utxos[k]) for k in sorted(utxos)]
280 if not leaves:
281 return sha256(b"BTFUTXO1|empty").hex()
282 layer = leaves
283 while len(layer) > 1:
284 nxt = []
285 for i in range(0, len(layer), 2):
286 left = layer[i]
287 right = layer[i + 1] if i + 1 < len(layer) else left
288 nxt.append(sha256(b"BTFNODE1|" + left + right))
289 layer = nxt
290 return layer[0].hex()
291
292
293def apply_blocks(blocks, strict_duplicates=False):
294 utxos = {}
295 spent = 0
296 missing_spends = []
297 duplicate_outputs = []
298 total_coinbase = 0
299 total_fees = 0
300 normal_txs = 0
301
302 for height, block in enumerate(blocks):
303 if block["computed_merkle_root"] != block["merkle_root"]:
304 raise ParseError("bad merkle root at height %d" % height)
305 for tx in block["transactions"]:
306 coinbase = is_coinbase(tx)
307 input_value = 0
308 output_value = sum(out["value_satoshis"] for out in tx["vout"])
309 if coinbase:
310 total_coinbase += output_value
311 else:
312 normal_txs += 1
313 for txin in tx["vin"]:
314 prev = "%s:%d" % (txin["prevout_hash"], txin["prevout_n"])
315 prev_utxo = utxos.pop(prev, None)
316 if prev_utxo is None:
317 missing_spends.append({
318 "height": height,
319 "txid": tx["txid"],
320 "missing_outpoint": prev,
321 })
322 else:
323 spent += prev_utxo["value_satoshis"]
324 input_value += prev_utxo["value_satoshis"]
325 if input_value >= output_value:
326 total_fees += input_value - output_value
327 for n, out in enumerate(tx["vout"]):
328 key = "%s:%d" % (tx["txid"], n)
329 if key in utxos:
330 duplicate_outputs.append({"height": height, "outpoint": key})
331 if strict_duplicates:
332 raise ParseError("duplicate output at height %d: %s" % (height, key))
333 utxos[key] = {
334 "txid": tx["txid"],
335 "vout": n,
336 "value_satoshis": out["value_satoshis"],
337 "value_btf": out["value_btf"],
338 "script_pub_key_hex": out["script_pub_key_hex"],
339 "created_height": height,
340 "created_block_hash": block["hash"],
341 "created_txid": tx["txid"],
342 "coinbase": coinbase,
343 }
344
345 total_unspent = sum(u["value_satoshis"] for u in utxos.values())
346 return {
347 "utxos": utxos,
348 "stats": {
349 "utxo_count": len(utxos),
350 "spent_outputs": spent,
351 "total_unspent_satoshis": total_unspent,
352 "total_unspent_btf": format_money(total_unspent),
353 "total_coinbase_satoshis": total_coinbase,
354 "total_coinbase_btf": format_money(total_coinbase),
355 "coinbase_minus_unspent_satoshis": total_coinbase - total_unspent,
356 "coinbase_minus_unspent_btf": format_money(total_coinbase - total_unspent),
357 "total_fees_satoshis": total_fees,
358 "total_fees_btf": format_money(total_fees),
359 "normal_transactions": normal_txs,
360 "missing_spends": missing_spends,
361 "duplicate_outputs": duplicate_outputs,
362 },
363 }
364
365
366def genesis_transaction():
367 script_sig = (
368 bytes.fromhex("04ffff001d")
369 + b"\x01\x04"
370 + bytes([len(GENESIS_TEXT)])
371 + GENESIS_TEXT.encode("ascii")
372 )
373 pubkey_num = bytes.fromhex(
374 "5f1df16b2b704c8a578d0bbaf74d385cde12c11ee50455f3c438ef4c3fbcf649"
375 "b6de611feae06279a60939e028a8d65c10b73071a6f16719274855feb0fd8a6704"
376 )
377 script_pub_key = bytes([65]) + pubkey_num[::-1] + b"\xac"
378 tx = {
379 "version": 1,
380 "vin": [{
381 "prevout_hash": "0" * 64,
382 "prevout_n": 0xFFFFFFFF,
383 "script_sig_hex": script_sig.hex(),
384 "sequence": 0xFFFFFFFF,
385 }],
386 "vout": [{
387 "value_satoshis": 50 * COIN,
388 "value_btf": format_money(50 * COIN),
389 "script_pub_key_hex": script_pub_key.hex(),
390 }],
391 "lock_time": 0,
392 }
393 tx["txid"] = hash_hex(ser_tx(tx))
394 return tx
395
396
397def reconstructed_genesis():
398 tx = genesis_transaction()
399 header = (
400 struct.pack("<i", 1)
401 + bytes(32)
402 + bytes.fromhex(tx["txid"])[::-1]
403 + struct.pack("<III", GENESIS_TIME, GENESIS_BITS, GENESIS_NONCE)
404 )
405 return {
406 "hash": hash_hex(header),
407 "version": 1,
408 "previous_hash": "0" * 64,
409 "merkle_root": tx["txid"],
410 "computed_merkle_root": tx["txid"],
411 "time": GENESIS_TIME,
412 "time_utc": utc(GENESIS_TIME),
413 "bits": "0x%08x" % GENESIS_BITS,
414 "nonce": GENESIS_NONCE,
415 "tx_count": 1,
416 "transactions": [tx],
417 "source_file": "reconstructed-from-source-constants",
418 "source_offset": 0,
419 "size": 80 + len(ser_compact_size(1)) + len(ser_tx(tx)),
420 }
421
422
423def ser_block(block):
424 header = (
425 struct.pack("<i", block["version"])
426 + bytes.fromhex(block["previous_hash"])[::-1]
427 + bytes.fromhex(block["merkle_root"])[::-1]
428 + struct.pack("<III", block["time"], int(block["bits"], 16), block["nonce"])
429 )
430 txs = block["transactions"]
431 return header + ser_compact_size(len(txs)) + b"".join(ser_tx(tx) for tx in txs)
432
433
434def format_money(value):
435 sign = "-" if value < 0 else ""
436 value = abs(value)
437 return "%s%d.%08d" % (sign, value // COIN, value % COIN)
438
439
440def utc(ts):
441 return _dt.datetime.fromtimestamp(ts, _dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
442
443
444def default_datadir():
445 if os.name == "nt" and os.environ.get("APPDATA"):
446 return os.path.join(os.environ["APPDATA"], "Bitflash")
447 if os.environ.get("HOME"):
448 return os.path.join(os.environ["HOME"], ".bitflash")
449 return "."
450
451
452def canonical_bytes(report):
453 return (json.dumps(report, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii")
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 @@
1#!/usr/bin/env python3 1#!/usr/bin/env python3
2# 2"""Build a static Bitflash block explorer from local blk*.dat files."""
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 3
4import argparse
5import hashlib
6import json
7import sys
8from pathlib import Path
9
10import bitflash_chain as chain
11
12
13ADDRESS_VERSION = 25
23B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" 14B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
15
16
24def b58check(payload): 17def b58check(payload):
25 v = bytes([ADDRESS_VERSION]) + payload 18 v = bytes([ADDRESS_VERSION]) + payload
26 v += sha256d(v)[:4] 19 v += chain.sha256d(v)[:4]
27 n = int.from_bytes(v, 'big') 20 n = int.from_bytes(v, "big")
28 s = "" 21 s = ""
29 while n > 0: 22 while n > 0:
30 n, r = divmod(n, 58); s = B58[r] + s 23 n, r = divmod(n, 58)
24 s = B58[r] + s
31 for byte in v: 25 for byte in v:
32 if byte == 0: s = "1" + s 26 if byte == 0:
33 else: break 27 s = "1" + s
28 else:
29 break
34 return s 30 return s
35 31
32
36def hash160(b): 33def hash160(b):
37 return hashlib.new('ripemd160', hashlib.sha256(b).digest()).digest() 34 return hashlib.new("ripemd160", hashlib.sha256(b).digest()).digest()
38 35
39class R: 36
40 def __init__(s, b, o=0): s.b = b; s.o = o 37def spk_info(script_hex):
41 def u(s, n): v = s.b[s.o:s.o+n]; s.o += n; return v 38 spk = bytes.fromhex(script_hex)
42 def i32(s): return struct.unpack('<i', s.u(4))[0] 39 if (
43 def u32(s): return struct.unpack('<I', s.u(4))[0] 40 len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xA9
44 def i64(s): return struct.unpack('<q', s.u(8))[0] 41 and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xAC
45 def cs(s): 42 ):
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])} 43 return {"type": "p2pkh", "address": b58check(spk[3:23])}
57 # P2PK: <pubkey> OP_CHECKSIG (65- or 33-byte key) 44 if len(spk) >= 35 and spk[-1] == 0xAC:
58 if len(spk) >= 35 and spk[-1] == 0xac:
59 if spk[0] == 0x41 and len(spk) == 67: 45 if spk[0] == 0x41 and len(spk) == 67:
60 pk = spk[1:66]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} 46 pk = spk[1:66]
47 return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))}
61 if spk[0] == 0x21 and len(spk) == 35: 48 if spk[0] == 0x21 and len(spk) == 35:
62 pk = spk[1:34]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} 49 pk = spk[1:34]
63 return {"type": "other", "hex": spk.hex()[:80]} 50 return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))}
64 51 return {"type": "other", "hex": script_hex[:80]}
65def parse_tx(r): 52
66 start = r.o 53
67 r.i32() 54def explorer_tx(tx):
68 nin = r.cs(); vin = [] 55 vin = []
69 for _ in range(nin): 56 for txin in tx["vin"]:
70 ph = r.u(32); idx = r.u32(); r.script(); r.u32() 57 coinbase = (
71 coinbase = (ph == b'\x00' * 32 and idx == 0xffffffff) 58 txin["prevout_hash"] == "0" * 64
72 vin.append({"coinbase": coinbase, "prev": (None if coinbase else ph[::-1].hex()), "vout": (None if coinbase else idx)}) 59 and txin["prevout_n"] == 0xFFFFFFFF
73 nout = r.cs(); vout = [] 60 )
74 for _ in range(nout): 61 vin.append({
75 val = r.i64(); spk = r.script(); vout.append(dict({"value": val}, **spk_info(spk))) 62 "coinbase": coinbase,
76 r.u32() 63 "prev": None if coinbase else txin["prevout_hash"],
77 raw = r.b[start:r.o] 64 "vout": None if coinbase else txin["prevout_n"],
78 return {"txid": sha256d(raw)[::-1].hex(), "vin": vin, "vout": vout, "size": len(raw)} 65 })
79 66 vout = []
80def main(): 67 for out in tx["vout"]:
81 if len(sys.argv) != 3: 68 vout.append(dict({"value": out["value_satoshis"]}, **spk_info(out["script_pub_key_hex"])))
82 print("usage: build-explorer.py <blk0001.dat> <out-dir>", file=sys.stderr); sys.exit(2) 69 return {"txid": tx["txid"], "vin": vin, "vout": vout, "size": len(chain.ser_tx(tx))}
83 blkpath, out = sys.argv[1], sys.argv[2] 70
84 data = open(blkpath, 'rb').read() 71
85 blocks = {}; o = 0; n = len(data) 72def parse_args(argv):
86 while o + 8 <= n: 73 ap = argparse.ArgumentParser(description=__doc__)
87 if data[o:o+4] != MAGIC: o += 1; continue 74 ap.add_argument("--datadir", help="Bitflash data directory containing blk*.dat")
88 o += 4; size = struct.unpack('<I', data[o:o+4])[0]; o += 4 75 ap.add_argument("--max-blocks", type=int, default=0,
89 blk = data[o:o+size]; o += size 76 help="number of blocks to scan; 0 means all block files")
90 if len(blk) < 80: continue 77 ap.add_argument("paths", nargs="+",
91 h = sha256d(blk[:80])[::-1].hex() 78 help="with --datadir: <out-dir>; otherwise: <blk*.dat>... <out-dir>")
92 r = R(blk, 0) 79 args = ap.parse_args(argv)
93 ver = r.i32(); prev = r.u(32)[::-1].hex(); merk = r.u(32)[::-1].hex() 80 if args.datadir:
94 t = r.u32(); bits = r.u32(); nonce = r.u32() 81 if len(args.paths) != 1:
95 ntx = r.cs(); txs = [] 82 raise SystemExit("usage with --datadir: build-explorer.py --datadir DATADIR <out-dir>")
96 try: 83 args.block_files = None
97 for _ in range(ntx): txs.append(parse_tx(r)) 84 args.out_dir = args.paths[0]
98 except Exception: 85 else:
99 continue 86 if len(args.paths) < 2:
100 blocks[h] = {"hash": h, "prev": prev, "merkleRoot": merk, "version": ver, 87 raise SystemExit("usage: build-explorer.py <blk*.dat>... <out-dir>")
101 "time": t, "bits": bits, "nonce": nonce, "size": size, "txs": txs} 88 args.block_files = args.paths[:-1]
102 if not blocks: 89 args.out_dir = args.paths[-1]
103 print("no blocks parsed", file=sys.stderr); sys.exit(1) 90 return args
104 91
105 hc = {} 92
106 def height(h): 93def load_blocks(args):
107 chain = []; cur = h 94 if args.datadir:
108 while cur in blocks and cur not in hc: 95 return chain.read_blocks(args.datadir, args.max_blocks)
109 chain.append(cur); cur = blocks[cur]["prev"] 96 return chain.read_blocks_from_files(args.block_files, args.max_blocks)
110 base = hc.get(cur, 0) 97
111 for hh in reversed(chain): base += 1; hc[hh] = base 98
112 return hc[h] 99def build_explorer(raw_blocks, out_dir):
113 for h in blocks: height(h) 100 try:
114 tip = max(blocks, key=lambda h: hc[h]) 101 blocks = chain.select_main_chain(raw_blocks)
115 chain = []; cur = tip 102 except chain.ParseError as e:
116 while cur in blocks: chain.append(cur); cur = blocks[cur]["prev"] 103 raise SystemExit(str(e))
117 chain.reverse() 104 out = Path(out_dir)
118 105 (out / "block").mkdir(parents=True, exist_ok=True)
119 os.makedirs(os.path.join(out, "block"), exist_ok=True) 106
120 summaries = [] 107 summaries = []
121 for h in chain: 108 for height, block in enumerate(blocks):
122 b = blocks[h]; H = hc[h] 109 txs = [explorer_tx(tx) for tx in block["transactions"]]
123 cb = b["txs"][0]["vout"][0] if b["txs"] and b["txs"][0]["vout"] else {} 110 detail = {
124 summaries.append({"height": H, "hash": h, "time": b["time"], "nTx": len(b["txs"]), 111 "height": height,
125 "size": b["size"], "cbValue": cb.get("value", 0), "cbAddr": cb.get("address", "")}) 112 "hash": block["hash"],
126 json.dump(dict({"height": H}, **b), open(os.path.join(out, "block", "%d.json" % H), "w"), separators=(',', ':')) 113 "prev": block["previous_hash"],
114 "merkleRoot": block["merkle_root"],
115 "version": block["version"],
116 "time": block["time"],
117 "bits": int(block["bits"], 16),
118 "nonce": block["nonce"],
119 "size": block["size"],
120 "txs": txs,
121 }
122 cb = txs[0]["vout"][0] if txs and txs[0]["vout"] else {}
123 summaries.append({
124 "height": height,
125 "hash": block["hash"],
126 "time": block["time"],
127 "nTx": len(txs),
128 "size": block["size"],
129 "cbValue": cb.get("value", 0),
130 "cbAddr": cb.get("address", ""),
131 })
132 (out / "block" / ("%d.json" % height)).write_text(
133 json.dumps(detail, sort_keys=True, separators=(",", ":")) + "\n",
134 encoding="ascii",
135 )
136
127 summaries.sort(key=lambda x: -x["height"]) 137 summaries.sort(key=lambda x: -x["height"])
128 json.dump({"network": "bitflash", "tipHeight": hc[tip], "count": len(chain), "blocks": summaries}, 138 (out / "blocks.json").write_text(
129 open(os.path.join(out, "blocks.json"), "w"), separators=(',', ':')) 139 json.dumps({
130 open(os.path.join(out, "index.html"), "w").write(INDEX_HTML) 140 "network": "bitflash",
131 print("explorer built: %d main-chain blocks, tip %d, into %s" % (len(chain), hc[tip], out)) 141 "tipHeight": len(blocks) - 1,
142 "count": len(blocks),
143 "blocks": summaries,
144 }, sort_keys=True, separators=(",", ":")) + "\n",
145 encoding="ascii",
146 )
147 (out / "index.html").write_text(INDEX_HTML, encoding="utf-8")
148 return len(blocks), len(blocks) - 1
149
150
151def main(argv):
152 args = parse_args(argv)
153 raw_blocks = load_blocks(args)
154 if not raw_blocks:
155 raise SystemExit("no Bitflash blocks parsed")
156 count, tip = build_explorer(raw_blocks, args.out_dir)
157 print("explorer built: %d main-chain blocks, tip %d, into %s" % (count, tip, args.out_dir))
158 return 0
132 159
133 160
134INDEX_HTML = r"""<!doctype html> 161INDEX_HTML = r"""<!doctype html>
@@ -144,6 +171,7 @@ INDEX_HTML = r"""<!doctype html>
144 body { font: 15px/1.5 system-ui, sans-serif; margin:0; background:var(--bg); color:var(--fg); } 171 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; } 172 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; } 173 h1 { font-size:18px; margin:0; }
174 h2 { font-size:16px; }
147 .mut { color:var(--mut); } 175 .mut { color:var(--mut); }
148 .wrap { padding:16px 24px; max-width:1100px; margin:0 auto; } 176 .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; } 177 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"""<!doctype html>
154 tr.blk:hover td { background:var(--card); } 182 tr.blk:hover td { background:var(--card); }
155 code { font-size:12px; word-break:break-all; } 183 code { font-size:12px; word-break:break-all; }
156 .scroll { overflow-x:auto; } 184 .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; } 185 .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; } 186 .kv { display:grid; grid-template-columns:150px 1fr; gap:4px 12px; font-size:13px; }
160 .kv div:nth-child(odd){ color:var(--mut); } 187 .kv div:nth-child(odd){ color:var(--mut); }
@@ -174,7 +201,7 @@ INDEX_HTML = r"""<!doctype html>
174 <button class="link" id="home" style="display:none">back to blocks</button> 201 <button class="link" id="home" style="display:none">back to blocks</button>
175</header> 202</header>
176<div class="wrap"> 203<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> 204 <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>
178 <div id="listview"> 205 <div id="listview">
179 <input id="q" placeholder="Search by height or block hash"> 206 <input id="q" placeholder="Search by height or block hash">
180 <div class="scroll"><table> 207 <div class="scroll"><table>
@@ -186,52 +213,114 @@ INDEX_HTML = r"""<!doctype html>
186 <div id="detail" style="display:none"></div> 213 <div id="detail" style="display:none"></div>
187</div> 214</div>
188<script> 215<script>
189var SUM = [], BYH = {}, SHOWN = 200; 216var SUM = [], SHOWN = 200;
190function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; } 217function 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'); } 218function 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];}); } 219function short(h){ return h ? h.slice(0,10)+"..."+h.slice(-6) : "-"; }
193function short(h){ return h ? h.slice(0,10)+"…"+h.slice(-6) : "-"; } 220function E(tag, cls, text){
194 221 var e = document.createElement(tag);
222 if(cls) e.className = cls;
223 if(text !== undefined) e.textContent = text;
224 return e;
225}
226function code(text){ return E('code', '', text); }
227function clear(e){ while(e.firstChild) e.removeChild(e.firstChild); }
228function append(parent){
229 for(var i=1;i<arguments.length;i++) parent.appendChild(arguments[i]);
230 return parent;
231}
232function row(cells, cls){
233 var tr = E('tr', cls || '');
234 cells.forEach(function(c){ append(tr, E('td', '', c)); });
235 return tr;
236}
195function renderList(filter){ 237function renderList(filter){
196 var q=(filter||"").trim().toLowerCase(), rows=document.getElementById('rows'), out=""; 238 var q=(filter||"").trim().toLowerCase(), rows=document.getElementById('rows');
239 clear(rows);
197 var list = q ? SUM.filter(function(b){ return String(b.height)===q || b.hash.indexOf(q)===0; }) : SUM.slice(0,SHOWN); 240 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]; 241 if(!list.length){
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>'; 242 var empty = E('tr');
243 var td = E('td', 'mut', 'no match');
244 td.colSpan = 6;
245 append(empty, td); append(rows, empty);
200 } 246 }
201 rows.innerHTML = out || '<tr><td colspan="6" class="mut">no match</td></tr>'; 247 list.forEach(function(b){
248 var tr = row([String(b.height), ts(b.time), String(b.nTx), b.size+' B', btf(b.cbValue), ''], 'blk');
249 tr.dataset.h = b.height;
250 clear(tr.children[5]);
251 append(tr.children[5], code(b.cbAddr || '-'));
252 tr.onclick = function(){ openBlock(tr.dataset.h); };
253 append(rows, tr);
254 });
202 document.getElementById('more').textContent = (!q && SUM.length>SHOWN) ? ("showing latest "+SHOWN+" of "+SUM.length+" blocks; search to find any") : ""; 255 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} 256}
205function ioSide(title, items, html){ 257function kv(parent, key, value, isCode){
206 var s='<div><div class="mut">'+title+'</div>'; for(var i=0;i<items.length;i++){ s+=html(items[i]); } return s+'</div>'; 258 append(parent, E('div', '', key));
259 append(parent, isCode ? append(E('div'), code(value)) : E('div', '', value));
260}
261function ioSide(title, items, render){
262 var side = E('div');
263 append(side, E('div', 'mut', title));
264 items.forEach(function(item){ append(side, render(item)); });
265 return side;
207} 266}
208function openBlock(h){ 267function openBlock(h){
209 fetch('block/'+h+'.json').then(function(r){return r.json();}).then(function(b){ 268 fetch('block/'+h+'.json').then(function(r){return r.json();}).then(function(b){
210 var s='<div class="card"><div class="kv">'+ 269 var detail = document.getElementById('detail');
211 '<div>Height</div><div>'+b.height+'</div>'+ 270 clear(detail);
212 '<div>Hash</div><div><code>'+esc(b.hash)+'</code></div>'+ 271 append(detail, E('h2', '', 'Block '+b.height));
213 '<div>Previous</div><div><button class="link" onclick="openBlock('+(b.height-1)+')"><code>'+esc(short(b.prev))+'</code></button></div>'+ 272 var card = E('div', 'card'), grid = E('div', 'kv');
214 '<div>Merkle root</div><div><code>'+esc(b.merkleRoot)+'</code></div>'+ 273 kv(grid, 'Height', String(b.height));
215 '<div>Time (UTC)</div><div>'+ts(b.time)+'</div>'+ 274 kv(grid, 'Hash', b.hash, true);
216 '<div>Bits / Nonce</div><div>'+b.bits.toString(16)+' / '+b.nonce+'</div>'+ 275 var prev = E('div');
217 '<div>Size / Txs</div><div>'+b.size+' B / '+b.txs.length+'</div>'+ 276 if(b.height > 0){
218 '</div></div>'; 277 var btn = E('button', 'link');
219 for(var i=0;i<b.txs.length;i++){ var t=b.txs[i]; 278 append(btn, code(short(b.prev)));
220 s+='<div class="tx"><div><code>'+esc(t.txid)+'</code> '+(i===0?'<span class="pill">coinbase</span>':'')+'</div><div class="io">'; 279 btn.onclick = function(){ openBlock(b.height - 1); };
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>'; }); 280 append(prev, btn);
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>'; }); 281 } else {
223 s+='</div></div>'; 282 append(prev, code(short(b.prev)));
224 } 283 }
284 append(grid, E('div', '', 'Previous'), prev);
285 kv(grid, 'Merkle root', b.merkleRoot, true);
286 kv(grid, 'Time (UTC)', ts(b.time));
287 kv(grid, 'Bits / Nonce', b.bits.toString(16)+' / '+b.nonce);
288 kv(grid, 'Size / Txs', b.size+' B / '+b.txs.length);
289 append(card, grid); append(detail, card);
290 b.txs.forEach(function(t, i){
291 var tx = E('div', 'tx');
292 var head = E('div');
293 append(head, code(t.txid));
294 if(i === 0) append(head, E('span', 'pill', 'coinbase'));
295 var io = E('div', 'io');
296 append(io, ioSide('Inputs', t.vin, function(v){
297 var d = E('div');
298 if(v.coinbase) append(d, E('span', 'pill', 'coinbase'));
299 else append(d, code(short(v.prev)+':'+v.vout));
300 return d;
301 }));
302 append(io, ioSide('Outputs', t.vout, function(o){
303 var d = E('div');
304 var to = o.address ? o.address : (o.pubkey ? 'pubkey '+o.pubkey.slice(0,16)+'...' : o.type);
305 append(d, document.createTextNode(btf(o.value)+' -> '), code(to));
306 return d;
307 }));
308 append(tx, head, io); append(detail, tx);
309 });
225 document.getElementById('listview').style.display='none'; 310 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; 311 detail.style.display='block';
227 document.getElementById('home').style.display='inline'; 312 document.getElementById('home').style.display='inline';
228 window.scrollTo(0,0); 313 window.scrollTo(0,0);
229 }); 314 });
230} 315}
231document.getElementById('home').onclick=function(){ document.getElementById('detail').style.display='none'; document.getElementById('listview').style.display='block'; this.style.display='none'; }; 316document.getElementById('home').onclick=function(){
317 document.getElementById('detail').style.display='none';
318 document.getElementById('listview').style.display='block';
319 this.style.display='none';
320};
232document.getElementById('q').oninput=function(){ renderList(this.value); }; 321document.getElementById('q').oninput=function(){ renderList(this.value); };
233fetch('blocks.json').then(function(r){return r.json();}).then(function(d){ 322fetch('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'; 323 SUM=d.blocks; document.getElementById('tip').textContent='tip height '+d.tipHeight+' - '+d.count+' blocks';
235 renderList(''); 324 renderList('');
236}); 325});
237</script> 326</script>
@@ -239,5 +328,6 @@ fetch('blocks.json').then(function(r){return r.json();}).then(function(d){
239</html> 328</html>
240""" 329"""
241 330
331
242if __name__ == "__main__": 332if __name__ == "__main__":
243 main() 333 raise SystemExit(main(sys.argv[1:]))
diff --git a/scripts/prove-utxo.py b/scripts/prove-utxo.py
index 7a10d2a..9f2704b 100644
--- a/scripts/prove-utxo.py
+++ b/scripts/prove-utxo.py
@@ -7,27 +7,16 @@ included in the deterministic UTXO root produced by verify-utxo-set.py.
7 7
8import argparse 8import argparse
9import hashlib 9import hashlib
10import importlib.util
11import json 10import json
12import os
13import sys 11import sys
14from pathlib import Path 12from pathlib import Path
15 13
14import bitflash_chain as chain
16 15
17SCHEMA = "bitflash-utxo-inclusion-proof-1" 16SCHEMA = "bitflash-utxo-inclusion-proof-1"
18NODE_DOMAIN = b"BTFNODE1|" 17NODE_DOMAIN = b"BTFNODE1|"
19 18
20 19
21def load_utxo_tool():
22 path = Path(__file__).with_name("verify-utxo-set.py")
23 spec = importlib.util.spec_from_file_location("bitflash_verify_utxo_set", path)
24 if spec is None or spec.loader is None:
25 raise SystemExit("could not load %s" % path)
26 mod = importlib.util.module_from_spec(spec)
27 spec.loader.exec_module(mod)
28 return mod
29
30
31def canonical_bytes(obj): 20def canonical_bytes(obj):
32 return (json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii") 21 return (json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii")
33 22
@@ -80,25 +69,27 @@ def build_merkle_proof(keys, leaf_hashes, index):
80 69
81 70
82def build_proof(args): 71def build_proof(args):
83 tool = load_utxo_tool()
84 txid, vout = split_outpoint(args.outpoint) 72 txid, vout = split_outpoint(args.outpoint)
85 outpoint = "%s:%d" % (txid, vout) 73 outpoint = "%s:%d" % (txid, vout)
86 74
87 raw_blocks = tool.read_blocks(args.datadir, args.max_blocks) 75 raw_blocks = chain.read_blocks(args.datadir, args.max_blocks)
88 if not raw_blocks: 76 if not raw_blocks:
89 raise SystemExit("no Bitflash blocks found in %s" % args.datadir) 77 raise SystemExit("no Bitflash blocks found in %s" % args.datadir)
90 blocks = tool.select_main_chain(raw_blocks) 78 try:
91 state = tool.apply_blocks(blocks) 79 blocks = chain.select_main_chain(raw_blocks)
80 state = chain.apply_blocks(blocks, strict_duplicates=args.strict_duplicates)
81 except chain.ParseError as e:
82 raise SystemExit(str(e))
92 utxos = state["utxos"] 83 utxos = state["utxos"]
93 stats = state["stats"] 84 stats = state["stats"]
94 if outpoint not in utxos: 85 if outpoint not in utxos:
95 raise SystemExit("outpoint is not unspent in the scanned chain: %s" % outpoint) 86 raise SystemExit("outpoint is not unspent in the scanned chain: %s" % outpoint)
96 87
97 keys = sorted(utxos) 88 keys = sorted(utxos)
98 leaf_hashes = [tool.utxo_leaf_hash(k, utxos[k]) for k in keys] 89 leaf_hashes = [chain.utxo_leaf_hash(k, utxos[k]) for k in keys]
99 index = keys.index(outpoint) 90 index = keys.index(outpoint)
100 proof_path, root = build_merkle_proof(keys, leaf_hashes, index) 91 proof_path, root = build_merkle_proof(keys, leaf_hashes, index)
101 direct_root = tool.utxo_root(utxos) 92 direct_root = chain.utxo_root(utxos)
102 if root != direct_root: 93 if root != direct_root:
103 raise SystemExit("internal proof root mismatch") 94 raise SystemExit("internal proof root mismatch")
104 95
@@ -110,8 +101,8 @@ def build_proof(args):
110 "chain": { 101 "chain": {
111 "name": "Bitflash", 102 "name": "Bitflash",
112 "ticker": "BTF", 103 "ticker": "BTF",
113 "message_start_hex": tool.MAGIC.hex(), 104 "message_start_hex": chain.MAGIC.hex(),
114 "coin": tool.COIN, 105 "coin": chain.COIN,
115 }, 106 },
116 "commitment": { 107 "commitment": {
117 "algorithm": "sorted-outpoint-merkle-sha256", 108 "algorithm": "sorted-outpoint-merkle-sha256",
@@ -169,20 +160,14 @@ def print_text(proof):
169 print("Verdict: %s" % proof["verdict"]) 160 print("Verdict: %s" % proof["verdict"])
170 161
171 162
172def default_datadir():
173 if os.name == "nt" and os.environ.get("APPDATA"):
174 return os.path.join(os.environ["APPDATA"], "Bitflash")
175 if os.environ.get("HOME"):
176 return os.path.join(os.environ["HOME"], ".bitflash")
177 return "."
178
179
180def main(argv): 163def main(argv):
181 ap = argparse.ArgumentParser(description=__doc__) 164 ap = argparse.ArgumentParser(description=__doc__)
182 ap.add_argument("outpoint", help="unspent output to prove, as txid:vout") 165 ap.add_argument("outpoint", help="unspent output to prove, as txid:vout")
183 ap.add_argument("--datadir", default=default_datadir(), help="Bitflash data directory") 166 ap.add_argument("--datadir", default=chain.default_datadir(), help="Bitflash data directory")
184 ap.add_argument("--max-blocks", type=int, default=0, 167 ap.add_argument("--max-blocks", type=int, default=0,
185 help="number of blocks to scan; 0 means all block files") 168 help="number of blocks to scan; 0 means all block files")
169 ap.add_argument("--strict-duplicates", action="store_true",
170 help="fail instead of using legacy last-write-wins duplicate output handling")
186 ap.add_argument("--json", action="store_true", help="print canonical JSON") 171 ap.add_argument("--json", action="store_true", help="print canonical JSON")
187 ap.add_argument("--out", help="write canonical JSON proof to this path") 172 ap.add_argument("--out", help="write canonical JSON proof to this path")
188 args = ap.parse_args(argv) 173 args = ap.parse_args(argv)
diff --git a/scripts/verify-fair-launch.py b/scripts/verify-fair-launch.py
index ce396fb..c33c8be 100644
--- a/scripts/verify-fair-launch.py
+++ b/scripts/verify-fair-launch.py
@@ -1,299 +1,12 @@
1#!/usr/bin/env python3 1#!/usr/bin/env python3
2"""Produce a deterministic Bitflash fair-launch audit report. 2"""Produce a deterministic Bitflash fair-launch audit report."""
3
4This verifier is intentionally self-contained. It parses Bitflash block files
5directly, without Berkeley DB, without the node binary, and without network
6access. The canonical JSON output is stable for the same inputs and can be
7timestamped later with OpenTimestamps or any other external notary.
8"""
9 3
10import argparse 4import argparse
11import datetime as _dt
12import hashlib 5import hashlib
13import json
14import os
15import struct
16import sys 6import sys
17from pathlib import Path 7from pathlib import Path
18 8
19 9import bitflash_chain as chain
20MAGIC = bytes([0xBF, 0x20, 0x5C, 0xFD])
21COIN = 100_000_000
22GENESIS_HASH = "5bd7cb255d814e48cebcdfb72da4dc87b34bd774227f8ceb546c5640f4bdc169"
23GENESIS_MERKLE = "1a45b4482532abb29b10e234d3f13132230525a339ecea91658ffa675a5b1325"
24GENESIS_TIME = 1_753_315_200
25GENESIS_BITS = 0x1F0FFFFF
26GENESIS_NONCE = 3141
27GENESIS_TEXT = "Bitflash 24/Jul/2026 Fair launch: one CPU one vote, no premine"
28
29
30class ParseError(Exception):
31 pass
32
33
34class Reader:
35 def __init__(self, data):
36 self.data = data
37 self.pos = 0
38
39 def take(self, n):
40 if self.pos + n > len(self.data):
41 raise ParseError("unexpected end of data")
42 out = self.data[self.pos:self.pos + n]
43 self.pos += n
44 return out
45
46 def u8(self):
47 return self.take(1)[0]
48
49 def u32(self):
50 return struct.unpack("<I", self.take(4))[0]
51
52 def i32(self):
53 return struct.unpack("<i", self.take(4))[0]
54
55 def i64(self):
56 return struct.unpack("<q", self.take(8))[0]
57
58
59def sha256d(data):
60 return hashlib.sha256(hashlib.sha256(data).digest()).digest()
61
62
63def hash_hex(serialized):
64 return sha256d(serialized)[::-1].hex()
65
66
67def compact_size(r):
68 first = r.u8()
69 if first < 253:
70 return first
71 if first == 253:
72 return struct.unpack("<H", r.take(2))[0]
73 if first == 254:
74 return r.u32()
75 return struct.unpack("<Q", r.take(8))[0]
76
77
78def read_varbytes(r):
79 return r.take(compact_size(r))
80
81
82def ser_compact_size(n):
83 if n < 253:
84 return bytes([n])
85 if n <= 0xFFFF:
86 return b"\xfd" + struct.pack("<H", n)
87 if n <= 0xFFFFFFFF:
88 return b"\xfe" + struct.pack("<I", n)
89 return b"\xff" + struct.pack("<Q", n)
90
91
92def ser_varbytes(b):
93 return ser_compact_size(len(b)) + b
94
95
96def ser_tx(tx):
97 out = bytearray()
98 out += struct.pack("<i", tx["version"])
99 out += ser_compact_size(len(tx["vin"]))
100 for txin in tx["vin"]:
101 out += bytes.fromhex(txin["prevout_hash"])[::-1]
102 out += struct.pack("<I", txin["prevout_n"])
103 out += ser_varbytes(bytes.fromhex(txin["script_sig_hex"]))
104 out += struct.pack("<I", txin["sequence"])
105 out += ser_compact_size(len(tx["vout"]))
106 for txout in tx["vout"]:
107 out += struct.pack("<q", txout["value_satoshis"])
108 out += ser_varbytes(bytes.fromhex(txout["script_pub_key_hex"]))
109 out += struct.pack("<I", tx["lock_time"])
110 return bytes(out)
111
112
113def parse_tx(r):
114 start = r.pos
115 tx = {"version": r.i32(), "vin": [], "vout": [], "lock_time": 0}
116 for _ in range(compact_size(r)):
117 prev_hash = r.take(32)[::-1].hex()
118 prev_n = r.u32()
119 script = read_varbytes(r).hex()
120 sequence = r.u32()
121 tx["vin"].append({
122 "prevout_hash": prev_hash,
123 "prevout_n": prev_n,
124 "script_sig_hex": script,
125 "sequence": sequence,
126 })
127 for _ in range(compact_size(r)):
128 value = r.i64()
129 script = read_varbytes(r).hex()
130 tx["vout"].append({
131 "value_satoshis": value,
132 "value_btf": format_money(value),
133 "script_pub_key_hex": script,
134 })
135 tx["lock_time"] = r.u32()
136 raw = r.data[start:r.pos]
137 tx["txid"] = hash_hex(raw)
138 return tx
139
140
141def merkle_root(txids):
142 if not txids:
143 return "0" * 64
144 layer = [bytes.fromhex(x)[::-1] for x in txids]
145 while len(layer) > 1:
146 nxt = []
147 for i in range(0, len(layer), 2):
148 left = layer[i]
149 right = layer[i + 1] if i + 1 < len(layer) else left
150 nxt.append(sha256d(left + right))
151 layer = nxt
152 return layer[0][::-1].hex()
153
154
155def parse_block(payload, file_name="", offset=0):
156 r = Reader(payload)
157 header = r.take(80)
158 version, = struct.unpack("<i", header[0:4])
159 prev = header[4:36][::-1].hex()
160 merkle = header[36:68][::-1].hex()
161 ntime, bits, nonce = struct.unpack("<III", header[68:80])
162 txs = [parse_tx(r) for _ in range(compact_size(r))]
163 if r.pos != len(payload):
164 raise ParseError("trailing bytes inside block")
165 txids = [tx["txid"] for tx in txs]
166 return {
167 "hash": hash_hex(header),
168 "version": version,
169 "previous_hash": prev,
170 "merkle_root": merkle,
171 "computed_merkle_root": merkle_root(txids),
172 "time": ntime,
173 "time_utc": utc(ntime),
174 "bits": "0x%08x" % bits,
175 "nonce": nonce,
176 "tx_count": len(txs),
177 "transactions": txs,
178 "source_file": file_name,
179 "source_offset": offset,
180 }
181
182
183def iter_block_files(datadir):
184 for path in sorted(Path(datadir).glob("blk*.dat")):
185 yield path
186
187
188def read_blocks(datadir, limit):
189 blocks = []
190 for path in iter_block_files(datadir):
191 data = path.read_bytes()
192 pos = 0
193 while True:
194 idx = data.find(MAGIC, pos)
195 if idx < 0:
196 break
197 if idx + 8 > len(data):
198 break
199 size = struct.unpack("<I", data[idx + 4:idx + 8])[0]
200 start = idx + 8
201 end = start + size
202 if end > len(data):
203 break
204 blocks.append(parse_block(data[start:end], path.name, start))
205 if limit and len(blocks) >= limit:
206 return blocks
207 pos = end
208 return blocks
209
210
211def genesis_transaction():
212 script_sig = (
213 bytes.fromhex("04ffff001d")
214 + b"\x01\x04"
215 + bytes([len(GENESIS_TEXT)])
216 + GENESIS_TEXT.encode("ascii")
217 )
218 pubkey_num = bytes.fromhex(
219 "5f1df16b2b704c8a578d0bbaf74d385cde12c11ee50455f3c438ef4c3fbcf649"
220 "b6de611feae06279a60939e028a8d65c10b73071a6f16719274855feb0fd8a6704"
221 )
222 # The genesis source uses CScript << CBigNum("0x..."), and CBigNum::getvch()
223 # returns the minimal little-endian script number representation.
224 script_pub_key = bytes([65]) + pubkey_num[::-1] + b"\xac"
225 tx = {
226 "version": 1,
227 "vin": [{
228 "prevout_hash": "0" * 64,
229 "prevout_n": 0xFFFFFFFF,
230 "script_sig_hex": script_sig.hex(),
231 "sequence": 0xFFFFFFFF,
232 }],
233 "vout": [{
234 "value_satoshis": 50 * COIN,
235 "value_btf": format_money(50 * COIN),
236 "script_pub_key_hex": script_pub_key.hex(),
237 }],
238 "lock_time": 0,
239 }
240 tx["txid"] = hash_hex(ser_tx(tx))
241 return tx
242
243
244def reconstructed_genesis():
245 tx = genesis_transaction()
246 header = (
247 struct.pack("<i", 1)
248 + bytes(32)
249 + bytes.fromhex(tx["txid"])[::-1]
250 + struct.pack("<III", GENESIS_TIME, GENESIS_BITS, GENESIS_NONCE)
251 )
252 return {
253 "hash": hash_hex(header),
254 "version": 1,
255 "previous_hash": "0" * 64,
256 "merkle_root": tx["txid"],
257 "computed_merkle_root": tx["txid"],
258 "time": GENESIS_TIME,
259 "time_utc": utc(GENESIS_TIME),
260 "bits": "0x%08x" % GENESIS_BITS,
261 "nonce": GENESIS_NONCE,
262 "tx_count": 1,
263 "transactions": [tx],
264 "source_file": "reconstructed-from-source-constants",
265 "source_offset": 0,
266 }
267
268
269def format_money(value):
270 sign = "-" if value < 0 else ""
271 value = abs(value)
272 return "%s%d.%08d" % (sign, value // COIN, value % COIN)
273
274
275def utc(ts):
276 return _dt.datetime.fromtimestamp(ts, _dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
277
278
279def default_datadir():
280 if os.name == "nt" and os.environ.get("APPDATA"):
281 return os.path.join(os.environ["APPDATA"], "Bitflash")
282 if os.environ.get("HOME"):
283 return os.path.join(os.environ["HOME"], ".bitflash")
284 return "."
285
286
287def is_coinbase(tx):
288 return (
289 len(tx["vin"]) == 1
290 and tx["vin"][0]["prevout_hash"] == "0" * 64
291 and tx["vin"][0]["prevout_n"] == 0xFFFFFFFF
292 )
293
294
295def canonical_bytes(report):
296 return (json.dumps(report, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii")
297 10
298 11
299def build_report(blocks, source_mode): 12def build_report(blocks, source_mode):
@@ -307,7 +20,7 @@ def build_report(blocks, source_mode):
307 if not block["transactions"]: 20 if not block["transactions"]:
308 continue 21 continue
309 cb = block["transactions"][0] 22 cb = block["transactions"][0]
310 if is_coinbase(cb): 23 if chain.is_coinbase(cb):
311 amount = sum(out["value_satoshis"] for out in cb["vout"]) 24 amount = sum(out["value_satoshis"] for out in cb["vout"])
312 total_mined += amount 25 total_mined += amount
313 coinbase_outputs.append({ 26 coinbase_outputs.append({
@@ -315,18 +28,18 @@ def build_report(blocks, source_mode):
315 "block_hash": block["hash"], 28 "block_hash": block["hash"],
316 "coinbase_txid": cb["txid"], 29 "coinbase_txid": cb["txid"],
317 "value_satoshis": amount, 30 "value_satoshis": amount,
318 "value_btf": format_money(amount), 31 "value_btf": chain.format_money(amount),
319 "outputs": cb["vout"], 32 "outputs": cb["vout"],
320 }) 33 })
321 non_coinbase_txs += sum(1 for tx in block["transactions"] if not is_coinbase(tx)) 34 non_coinbase_txs += sum(1 for tx in block["transactions"] if not chain.is_coinbase(tx))
322 35
323 checks = { 36 checks = {
324 "genesis_hash_matches_source": genesis["hash"] == GENESIS_HASH, 37 "genesis_hash_matches_source": genesis["hash"] == chain.GENESIS_HASH,
325 "genesis_merkle_matches_source": genesis["merkle_root"] == GENESIS_MERKLE, 38 "genesis_merkle_matches_source": genesis["merkle_root"] == chain.GENESIS_MERKLE,
326 "computed_merkle_matches_header": genesis["computed_merkle_root"] == genesis["merkle_root"], 39 "computed_merkle_matches_header": genesis["computed_merkle_root"] == genesis["merkle_root"],
327 "genesis_has_single_transaction": genesis["tx_count"] == 1, 40 "genesis_has_single_transaction": genesis["tx_count"] == 1,
328 "genesis_transaction_is_coinbase": bool(genesis["transactions"] and is_coinbase(genesis["transactions"][0])), 41 "genesis_transaction_is_coinbase": bool(genesis["transactions"] and chain.is_coinbase(genesis["transactions"][0])),
329 "genesis_supply_is_50_btf": sum(out["value_satoshis"] for out in launch_outputs) == 50 * COIN, 42 "genesis_supply_is_50_btf": sum(out["value_satoshis"] for out in launch_outputs) == 50 * chain.COIN,
330 } 43 }
331 44
332 report = { 45 report = {
@@ -336,17 +49,17 @@ def build_report(blocks, source_mode):
336 "chain": { 49 "chain": {
337 "name": "Bitflash", 50 "name": "Bitflash",
338 "ticker": "BTF", 51 "ticker": "BTF",
339 "message_start_hex": MAGIC.hex(), 52 "message_start_hex": chain.MAGIC.hex(),
340 "coin": COIN, 53 "coin": chain.COIN,
341 }, 54 },
342 "source_constants": { 55 "source_constants": {
343 "genesis_hash": GENESIS_HASH, 56 "genesis_hash": chain.GENESIS_HASH,
344 "genesis_merkle_root": GENESIS_MERKLE, 57 "genesis_merkle_root": chain.GENESIS_MERKLE,
345 "genesis_time": GENESIS_TIME, 58 "genesis_time": chain.GENESIS_TIME,
346 "genesis_time_utc": utc(GENESIS_TIME), 59 "genesis_time_utc": chain.utc(chain.GENESIS_TIME),
347 "genesis_bits": "0x%08x" % GENESIS_BITS, 60 "genesis_bits": "0x%08x" % chain.GENESIS_BITS,
348 "genesis_nonce": GENESIS_NONCE, 61 "genesis_nonce": chain.GENESIS_NONCE,
349 "genesis_message": GENESIS_TEXT, 62 "genesis_message": chain.GENESIS_TEXT,
350 }, 63 },
351 "scanned": { 64 "scanned": {
352 "block_count": len(blocks), 65 "block_count": len(blocks),
@@ -354,7 +67,7 @@ def build_report(blocks, source_mode):
354 "last_height": len(blocks) - 1, 67 "last_height": len(blocks) - 1,
355 "non_coinbase_transactions": non_coinbase_txs, 68 "non_coinbase_transactions": non_coinbase_txs,
356 "total_coinbase_satoshis": total_mined, 69 "total_coinbase_satoshis": total_mined,
357 "total_coinbase_btf": format_money(total_mined), 70 "total_coinbase_btf": chain.format_money(total_mined),
358 }, 71 },
359 "genesis": { 72 "genesis": {
360 "hash": genesis["hash"], 73 "hash": genesis["hash"],
@@ -381,7 +94,7 @@ def build_report(blocks, source_mode):
381 ], 94 ],
382 }, 95 },
383 } 96 }
384 report["attestation_payload_sha256"] = hashlib.sha256(canonical_bytes(report)).hexdigest() 97 report["attestation_payload_sha256"] = hashlib.sha256(chain.canonical_bytes(report)).hexdigest()
385 return report 98 return report
386 99
387 100
@@ -397,7 +110,7 @@ def print_text(report):
397 print(" merkle: %s" % report["genesis"]["merkle_root"]) 110 print(" merkle: %s" % report["genesis"]["merkle_root"])
398 print(" time: %s" % report["genesis"]["time_utc"]) 111 print(" time: %s" % report["genesis"]["time_utc"])
399 print(" txs: %d" % report["genesis"]["tx_count"]) 112 print(" txs: %d" % report["genesis"]["tx_count"])
400 print(" supply: %s BTF" % format_money(sum(o["value_satoshis"] for o in report["genesis"]["outputs"]))) 113 print(" supply: %s BTF" % chain.format_money(sum(o["value_satoshis"] for o in report["genesis"]["outputs"])))
401 print() 114 print()
402 print("Scanned") 115 print("Scanned")
403 print(" blocks: %d" % report["scanned"]["block_count"]) 116 print(" blocks: %d" % report["scanned"]["block_count"])
@@ -417,7 +130,7 @@ def print_text(report):
417 130
418def main(argv): 131def main(argv):
419 ap = argparse.ArgumentParser(description=__doc__) 132 ap = argparse.ArgumentParser(description=__doc__)
420 ap.add_argument("--datadir", default=default_datadir(), help="Bitflash data directory") 133 ap.add_argument("--datadir", default=chain.default_datadir(), help="Bitflash data directory")
421 ap.add_argument("--max-blocks", type=int, default=100, help="number of blocks to scan from blk*.dat") 134 ap.add_argument("--max-blocks", type=int, default=100, help="number of blocks to scan from blk*.dat")
422 ap.add_argument("--json", action="store_true", help="print canonical JSON") 135 ap.add_argument("--json", action="store_true", help="print canonical JSON")
423 ap.add_argument("--out", help="write canonical JSON to this path") 136 ap.add_argument("--out", help="write canonical JSON to this path")
@@ -426,18 +139,19 @@ def main(argv):
426 args = ap.parse_args(argv) 139 args = ap.parse_args(argv)
427 140
428 if args.reconstruct_genesis: 141 if args.reconstruct_genesis:
429 blocks = [reconstructed_genesis()] 142 blocks = [chain.reconstructed_genesis()]
430 source_mode = "reconstructed-genesis" 143 source_mode = "reconstructed-genesis"
431 datadir = ""
432 else: 144 else:
433 datadir = Path(args.datadir) 145 blocks = chain.read_blocks(args.datadir, args.max_blocks)
434 blocks = read_blocks(datadir, args.max_blocks)
435 if not blocks: 146 if not blocks:
436 raise SystemExit("no Bitflash blocks found in %s; use --reconstruct-genesis for a source-only check" % datadir) 147 raise SystemExit("no Bitflash blocks found in %s; use --reconstruct-genesis for a source-only check" % args.datadir)
437 source_mode = "local-block-files" 148 source_mode = "local-block-files"
438 149
439 report = build_report(blocks, source_mode) 150 try:
440 data = canonical_bytes(report) 151 report = build_report(blocks, source_mode)
152 except chain.ParseError as e:
153 raise SystemExit(str(e))
154 data = chain.canonical_bytes(report)
441 155
442 if args.out: 156 if args.out:
443 Path(args.out).write_bytes(data) 157 Path(args.out).write_bytes(data)
diff --git a/scripts/verify-utxo-proof.py b/scripts/verify-utxo-proof.py
index 31099f7..c29caa9 100644
--- a/scripts/verify-utxo-proof.py
+++ b/scripts/verify-utxo-proof.py
@@ -7,6 +7,7 @@ import json
7import sys 7import sys
8from pathlib import Path 8from pathlib import Path
9 9
10import bitflash_chain as chain
10 11
11SCHEMA = "bitflash-utxo-inclusion-proof-1" 12SCHEMA = "bitflash-utxo-inclusion-proof-1"
12LEAF_DOMAIN = "BTFUTXO1" 13LEAF_DOMAIN = "BTFUTXO1"
@@ -17,22 +18,6 @@ def canonical_bytes(obj):
17 return (json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii") 18 return (json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii")
18 19
19 20
20def sha256(data):
21 return hashlib.sha256(data).digest()
22
23
24def utxo_leaf_hash(outpoint, utxo):
25 payload = "|".join([
26 LEAF_DOMAIN,
27 outpoint,
28 str(utxo["value_satoshis"]),
29 utxo["script_pub_key_hex"],
30 str(utxo["created_height"]),
31 utxo["created_txid"],
32 ]).encode("ascii")
33 return sha256(payload)
34
35
36def load_proof(path): 21def load_proof(path):
37 try: 22 try:
38 return json.loads(Path(path).read_text(encoding="ascii")) 23 return json.loads(Path(path).read_text(encoding="ascii"))
@@ -59,7 +44,7 @@ def verify(proof, expected_root=None):
59 failures.append("target outpoint does not match txid:vout") 44 failures.append("target outpoint does not match txid:vout")
60 45
61 try: 46 try:
62 current = utxo_leaf_hash(outpoint, utxo) 47 current = chain.utxo_leaf_hash(outpoint, utxo)
63 leaf_ok = target.get("leaf_hash") == current.hex() 48 leaf_ok = target.get("leaf_hash") == current.hex()
64 except Exception as e: 49 except Exception as e:
65 failures.append("could not hash target leaf: %s" % e) 50 failures.append("could not hash target leaf: %s" % e)
@@ -79,9 +64,9 @@ def verify(proof, expected_root=None):
79 failures.append("bad sibling hash length at proof level %d" % i) 64 failures.append("bad sibling hash length at proof level %d" % i)
80 side = step.get("side") 65 side = step.get("side")
81 if side == "right": 66 if side == "right":
82 current = sha256(NODE_DOMAIN + current + sibling) 67 current = chain.sha256(NODE_DOMAIN + current + sibling)
83 elif side == "left": 68 elif side == "left":
84 current = sha256(NODE_DOMAIN + sibling + current) 69 current = chain.sha256(NODE_DOMAIN + sibling + current)
85 else: 70 else:
86 failures.append("bad sibling side at proof level %d" % i) 71 failures.append("bad sibling side at proof level %d" % i)
87 72
diff --git a/scripts/verify-utxo-set.py b/scripts/verify-utxo-set.py
index 12b024f..b5bf3d5 100644
--- a/scripts/verify-utxo-set.py
+++ b/scripts/verify-utxo-set.py
@@ -1,363 +1,24 @@
1#!/usr/bin/env python3 1#!/usr/bin/env python3
2"""Build a deterministic Bitflash UTXO-set commitment from local blk*.dat. 2"""Build a deterministic Bitflash UTXO-set commitment from local blk*.dat."""
3
4This is deliberately outside consensus. It lets auditors reconstruct the chain
5state from disk blocks and compare the same canonical UTXO root independently.
6"""
7 3
8import argparse 4import argparse
9import datetime as _dt
10import hashlib 5import hashlib
11import json
12import os
13import struct
14import sys 6import sys
15from pathlib import Path 7from pathlib import Path
16 8
17 9import bitflash_chain as chain
18MAGIC = bytes([0xBF, 0x20, 0x5C, 0xFD])
19COIN = 100_000_000
20GENESIS_HASH = "5bd7cb255d814e48cebcdfb72da4dc87b34bd774227f8ceb546c5640f4bdc169"
21GENESIS_MERKLE = "1a45b4482532abb29b10e234d3f13132230525a339ecea91658ffa675a5b1325"
22
23
24class ParseError(Exception):
25 pass
26
27
28class Reader:
29 def __init__(self, data):
30 self.data = data
31 self.pos = 0
32
33 def take(self, n):
34 if self.pos + n > len(self.data):
35 raise ParseError("unexpected end of data")
36 out = self.data[self.pos:self.pos + n]
37 self.pos += n
38 return out
39
40 def u8(self):
41 return self.take(1)[0]
42
43 def u32(self):
44 return struct.unpack("<I", self.take(4))[0]
45
46 def i32(self):
47 return struct.unpack("<i", self.take(4))[0]
48
49 def i64(self):
50 return struct.unpack("<q", self.take(8))[0]
51
52
53def sha256(data):
54 return hashlib.sha256(data).digest()
55
56
57def sha256d(data):
58 return hashlib.sha256(hashlib.sha256(data).digest()).digest()
59
60
61def hash_hex(serialized):
62 return sha256d(serialized)[::-1].hex()
63
64
65def compact_size(r):
66 first = r.u8()
67 if first < 253:
68 return first
69 if first == 253:
70 return struct.unpack("<H", r.take(2))[0]
71 if first == 254:
72 return r.u32()
73 return struct.unpack("<Q", r.take(8))[0]
74
75
76def read_varbytes(r):
77 return r.take(compact_size(r))
78
79
80def ser_compact_size(n):
81 if n < 253:
82 return bytes([n])
83 if n <= 0xFFFF:
84 return b"\xfd" + struct.pack("<H", n)
85 if n <= 0xFFFFFFFF:
86 return b"\xfe" + struct.pack("<I", n)
87 return b"\xff" + struct.pack("<Q", n)
88
89
90def ser_varbytes(b):
91 return ser_compact_size(len(b)) + b
92
93
94def ser_tx(tx):
95 out = bytearray()
96 out += struct.pack("<i", tx["version"])
97 out += ser_compact_size(len(tx["vin"]))
98 for txin in tx["vin"]:
99 out += bytes.fromhex(txin["prevout_hash"])[::-1]
100 out += struct.pack("<I", txin["prevout_n"])
101 out += ser_varbytes(bytes.fromhex(txin["script_sig_hex"]))
102 out += struct.pack("<I", txin["sequence"])
103 out += ser_compact_size(len(tx["vout"]))
104 for txout in tx["vout"]:
105 out += struct.pack("<q", txout["value_satoshis"])
106 out += ser_varbytes(bytes.fromhex(txout["script_pub_key_hex"]))
107 out += struct.pack("<I", tx["lock_time"])
108 return bytes(out)
109
110
111def parse_tx(r):
112 tx = {"version": r.i32(), "vin": [], "vout": [], "lock_time": 0}
113 for _ in range(compact_size(r)):
114 tx["vin"].append({
115 "prevout_hash": r.take(32)[::-1].hex(),
116 "prevout_n": r.u32(),
117 "script_sig_hex": read_varbytes(r).hex(),
118 "sequence": r.u32(),
119 })
120 for _ in range(compact_size(r)):
121 value = r.i64()
122 tx["vout"].append({
123 "value_satoshis": value,
124 "value_btf": format_money(value),
125 "script_pub_key_hex": read_varbytes(r).hex(),
126 })
127 tx["lock_time"] = r.u32()
128 tx["txid"] = hash_hex(ser_tx(tx))
129 return tx
130
131
132def merkle_root(txids):
133 if not txids:
134 return "0" * 64
135 layer = [bytes.fromhex(x)[::-1] for x in txids]
136 while len(layer) > 1:
137 nxt = []
138 for i in range(0, len(layer), 2):
139 left = layer[i]
140 right = layer[i + 1] if i + 1 < len(layer) else left
141 nxt.append(sha256d(left + right))
142 layer = nxt
143 return layer[0][::-1].hex()
144
145
146def parse_block(payload, file_name="", offset=0):
147 r = Reader(payload)
148 header = r.take(80)
149 version, = struct.unpack("<i", header[0:4])
150 prev = header[4:36][::-1].hex()
151 merkle = header[36:68][::-1].hex()
152 ntime, bits, nonce = struct.unpack("<III", header[68:80])
153 txs = [parse_tx(r) for _ in range(compact_size(r))]
154 if r.pos != len(payload):
155 raise ParseError("trailing bytes inside block")
156 txids = [tx["txid"] for tx in txs]
157 return {
158 "hash": hash_hex(header),
159 "version": version,
160 "previous_hash": prev,
161 "merkle_root": merkle,
162 "computed_merkle_root": merkle_root(txids),
163 "time": ntime,
164 "time_utc": utc(ntime),
165 "bits": "0x%08x" % bits,
166 "nonce": nonce,
167 "tx_count": len(txs),
168 "transactions": txs,
169 "source_file": file_name,
170 "source_offset": offset,
171 }
172
173
174def read_blocks(datadir, max_blocks):
175 blocks = []
176 for path in sorted(Path(datadir).glob("blk*.dat")):
177 data = path.read_bytes()
178 pos = 0
179 while True:
180 idx = data.find(MAGIC, pos)
181 if idx < 0:
182 break
183 if idx + 8 > len(data):
184 break
185 size = struct.unpack("<I", data[idx + 4:idx + 8])[0]
186 start = idx + 8
187 end = start + size
188 if end > len(data):
189 break
190 blocks.append(parse_block(data[start:end], path.name, start))
191 if max_blocks and len(blocks) >= max_blocks:
192 return blocks
193 pos = end
194 return blocks
195
196
197def select_main_chain(blocks):
198 by_hash = {}
199 for block in blocks:
200 by_hash.setdefault(block["hash"], block)
201 if GENESIS_HASH not in by_hash:
202 raise ParseError("genesis block not found in block files")
203
204 children = {}
205 for block in by_hash.values():
206 children.setdefault(block["previous_hash"], []).append(block)
207 for v in children.values():
208 v.sort(key=lambda b: (b["hash"], b["source_file"], b["source_offset"]))
209
210 best = []
211 stack = [(by_hash[GENESIS_HASH], [by_hash[GENESIS_HASH]])]
212 while stack:
213 block, path = stack.pop()
214 if len(path) > len(best) or (len(path) == len(best) and block["hash"] < best[-1]["hash"]):
215 best = path
216 for child in children.get(block["hash"], []):
217 stack.append((child, path + [child]))
218 return best
219
220
221def is_coinbase(tx):
222 return (
223 len(tx["vin"]) == 1
224 and tx["vin"][0]["prevout_hash"] == "0" * 64
225 and tx["vin"][0]["prevout_n"] == 0xFFFFFFFF
226 )
227
228
229def utxo_leaf_hash(outpoint, utxo):
230 # Text fields are length-delimited by separators and all variable data is
231 # hex or decimal ASCII. That keeps the commitment language-neutral.
232 payload = "|".join([
233 "BTFUTXO1",
234 outpoint,
235 str(utxo["value_satoshis"]),
236 utxo["script_pub_key_hex"],
237 str(utxo["created_height"]),
238 utxo["created_txid"],
239 ]).encode("ascii")
240 return sha256(payload)
241
242
243def utxo_root(utxos):
244 leaves = [utxo_leaf_hash(k, utxos[k]) for k in sorted(utxos)]
245 if not leaves:
246 return sha256(b"BTFUTXO1|empty").hex()
247 layer = leaves
248 while len(layer) > 1:
249 nxt = []
250 for i in range(0, len(layer), 2):
251 left = layer[i]
252 right = layer[i + 1] if i + 1 < len(layer) else left
253 nxt.append(sha256(b"BTFNODE1|" + left + right))
254 layer = nxt
255 return layer[0].hex()
256
257
258def apply_blocks(blocks):
259 utxos = {}
260 spent = 0
261 missing_spends = []
262 duplicate_outputs = []
263 total_coinbase = 0
264 total_fees = 0
265 normal_txs = 0
266
267 for height, block in enumerate(blocks):
268 if block["computed_merkle_root"] != block["merkle_root"]:
269 raise ParseError("bad merkle root at height %d" % height)
270 for tx_index, tx in enumerate(block["transactions"]):
271 coinbase = is_coinbase(tx)
272 input_value = 0
273 output_value = sum(out["value_satoshis"] for out in tx["vout"])
274 if coinbase:
275 total_coinbase += output_value
276 else:
277 normal_txs += 1
278 for txin in tx["vin"]:
279 prev = "%s:%d" % (txin["prevout_hash"], txin["prevout_n"])
280 prev_utxo = utxos.pop(prev, None)
281 if prev_utxo is None:
282 missing_spends.append({
283 "height": height,
284 "txid": tx["txid"],
285 "missing_outpoint": prev,
286 })
287 else:
288 spent += prev_utxo["value_satoshis"]
289 input_value += prev_utxo["value_satoshis"]
290 if input_value >= output_value:
291 total_fees += input_value - output_value
292 for n, out in enumerate(tx["vout"]):
293 key = "%s:%d" % (tx["txid"], n)
294 if key in utxos:
295 duplicate_outputs.append({"height": height, "outpoint": key})
296 utxos[key] = {
297 "txid": tx["txid"],
298 "vout": n,
299 "value_satoshis": out["value_satoshis"],
300 "value_btf": out["value_btf"],
301 "script_pub_key_hex": out["script_pub_key_hex"],
302 "created_height": height,
303 "created_block_hash": block["hash"],
304 "created_txid": tx["txid"],
305 "coinbase": coinbase,
306 }
307
308 total_unspent = sum(u["value_satoshis"] for u in utxos.values())
309 return {
310 "utxos": utxos,
311 "stats": {
312 "utxo_count": len(utxos),
313 "spent_outputs": spent,
314 "total_unspent_satoshis": total_unspent,
315 "total_unspent_btf": format_money(total_unspent),
316 "total_coinbase_satoshis": total_coinbase,
317 "total_coinbase_btf": format_money(total_coinbase),
318 "coinbase_minus_unspent_satoshis": total_coinbase - total_unspent,
319 "coinbase_minus_unspent_btf": format_money(total_coinbase - total_unspent),
320 "total_fees_satoshis": total_fees,
321 "total_fees_btf": format_money(total_fees),
322 "normal_transactions": normal_txs,
323 "missing_spends": missing_spends,
324 "duplicate_outputs": duplicate_outputs,
325 },
326 }
327
328
329def format_money(value):
330 sign = "-" if value < 0 else ""
331 value = abs(value)
332 return "%s%d.%08d" % (sign, value // COIN, value % COIN)
333
334
335def utc(ts):
336 return _dt.datetime.fromtimestamp(ts, _dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
337
338
339def default_datadir():
340 if os.name == "nt" and os.environ.get("APPDATA"):
341 return os.path.join(os.environ["APPDATA"], "Bitflash")
342 if os.environ.get("HOME"):
343 return os.path.join(os.environ["HOME"], ".bitflash")
344 return "."
345
346
347def canonical_bytes(report):
348 return (json.dumps(report, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii")
349 10
350 11
351def build_report(raw_blocks, include_utxos=False): 12def build_report(raw_blocks, include_utxos=False, strict_duplicates=False):
352 blocks = select_main_chain(raw_blocks) 13 blocks = chain.select_main_chain(raw_blocks)
353 state = apply_blocks(blocks) 14 state = chain.apply_blocks(blocks, strict_duplicates=strict_duplicates)
354 utxos = state["utxos"] 15 utxos = state["utxos"]
355 stats = state["stats"] 16 stats = state["stats"]
356 tip = blocks[-1] 17 tip = blocks[-1]
357 root = utxo_root(utxos) 18 root = chain.utxo_root(utxos)
358 checks = { 19 checks = {
359 "genesis_hash_matches_source": blocks[0]["hash"] == GENESIS_HASH, 20 "genesis_hash_matches_source": blocks[0]["hash"] == chain.GENESIS_HASH,
360 "genesis_merkle_matches_source": blocks[0]["merkle_root"] == GENESIS_MERKLE, 21 "genesis_merkle_matches_source": blocks[0]["merkle_root"] == chain.GENESIS_MERKLE,
361 "all_scanned_merkle_roots_match": all(b["computed_merkle_root"] == b["merkle_root"] for b in blocks), 22 "all_scanned_merkle_roots_match": all(b["computed_merkle_root"] == b["merkle_root"] for b in blocks),
362 "no_missing_spends": len(stats["missing_spends"]) == 0, 23 "no_missing_spends": len(stats["missing_spends"]) == 0,
363 } 24 }
@@ -370,8 +31,8 @@ def build_report(raw_blocks, include_utxos=False):
370 "chain": { 31 "chain": {
371 "name": "Bitflash", 32 "name": "Bitflash",
372 "ticker": "BTF", 33 "ticker": "BTF",
373 "message_start_hex": MAGIC.hex(), 34 "message_start_hex": chain.MAGIC.hex(),
374 "coin": COIN, 35 "coin": chain.COIN,
375 }, 36 },
376 "commitment": { 37 "commitment": {
377 "algorithm": "sorted-outpoint-merkle-sha256", 38 "algorithm": "sorted-outpoint-merkle-sha256",
@@ -400,7 +61,7 @@ def build_report(raw_blocks, include_utxos=False):
400 61
401 62
402def print_text(report): 63def print_text(report):
403 data_hash = hashlib.sha256(canonical_bytes(report)).hexdigest() 64 data_hash = hashlib.sha256(chain.canonical_bytes(report)).hexdigest()
404 print("Bitflash UTXO-set commitment verifier") 65 print("Bitflash UTXO-set commitment verifier")
405 print("schema: %s" % report["schema"]) 66 print("schema: %s" % report["schema"])
406 print("canonical report sha256: %s" % data_hash) 67 print("canonical report sha256: %s" % data_hash)
@@ -437,20 +98,26 @@ def print_text(report):
437 98
438def main(argv): 99def main(argv):
439 ap = argparse.ArgumentParser(description=__doc__) 100 ap = argparse.ArgumentParser(description=__doc__)
440 ap.add_argument("--datadir", default=default_datadir(), help="Bitflash data directory") 101 ap.add_argument("--datadir", default=chain.default_datadir(), help="Bitflash data directory")
441 ap.add_argument("--max-blocks", type=int, default=0, 102 ap.add_argument("--max-blocks", type=int, default=0,
442 help="number of blocks to scan; 0 means all block files") 103 help="number of blocks to scan; 0 means all block files")
443 ap.add_argument("--include-utxos", action="store_true", 104 ap.add_argument("--include-utxos", action="store_true",
444 help="include the full sorted UTXO map in JSON output") 105 help="include the full sorted UTXO map in JSON output")
106 ap.add_argument("--strict-duplicates", action="store_true",
107 help="fail instead of using legacy last-write-wins duplicate output handling")
445 ap.add_argument("--json", action="store_true", help="print canonical JSON") 108 ap.add_argument("--json", action="store_true", help="print canonical JSON")
446 ap.add_argument("--out", help="write canonical JSON to this path") 109 ap.add_argument("--out", help="write canonical JSON to this path")
447 args = ap.parse_args(argv) 110 args = ap.parse_args(argv)
448 111
449 blocks = read_blocks(args.datadir, args.max_blocks) 112 blocks = chain.read_blocks(args.datadir, args.max_blocks)
450 if not blocks: 113 if not blocks:
451 raise SystemExit("no Bitflash blocks found in %s" % args.datadir) 114 raise SystemExit("no Bitflash blocks found in %s" % args.datadir)
452 report = build_report(blocks, include_utxos=args.include_utxos) 115 try:
453 data = canonical_bytes(report) 116 report = build_report(blocks, include_utxos=args.include_utxos,
117 strict_duplicates=args.strict_duplicates)
118 except chain.ParseError as e:
119 raise SystemExit(str(e))
120 data = chain.canonical_bytes(report)
454 if args.out: 121 if args.out:
455 Path(args.out).write_bytes(data) 122 Path(args.out).write_bytes(data)
456 if args.json: 123 if args.json: