upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbitflash <admin@bitflash.network>2026-08-11 09:22:23 -0300
committerbitflash <admin@bitflash.network>2026-08-11 09:22:23 -0300
commiteb2838e3b12079f78f7d86a5b91beffd84b8a048 (patch)
tree315d8e1d011d0496846357c391b9122804384920
parentccf4489dbdf9e9d691e02e867562518c92168007 (diff)
parent2482df9e1ac0706233e8e172dd68e6465db526c7 (diff)
Merge main into utxo-set-commitment (union README)
-rw-r--r--README.md10
-rw-r--r--docs/fair-launch.md69
-rw-r--r--scripts/verify-fair-launch.py452
3 files changed, 531 insertions, 0 deletions
diff --git a/README.md b/README.md
index 0827ac1..6fa59a5 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,16 @@ It reconstructs the best chain from local block files, computes the current
83UTXO root and supply, and writes canonical JSON that independent auditors can 83UTXO root and supply, and writes canonical JSON that independent auditors can
84compare or timestamp externally with OpenTimestamps. See 84compare or timestamp externally with OpenTimestamps. See
85[UTXO-set commitments](docs/utxo-commitment.md). 85[UTXO-set commitments](docs/utxo-commitment.md).
86Bitflash also ships a deterministic fair-launch verifier:
87
88```bash
89python3 scripts/verify-fair-launch.py --max-blocks 1000 --out bitflash-fair-launch-report.json --json
90```
91
92The report reads the local chain directly, checks the genesis launch baseline,
93and produces canonical JSON whose hash can be compared by independent auditors
94or timestamped externally with OpenTimestamps. See
95[fair-launch verification](docs/fair-launch.md).
86 96
87**Keep your node current.** Consensus rules have changed since the first 97**Keep your node current.** Consensus rules have changed since the first
88releases — 1.2.1 fixed a bug that let anyone spend anyone's coins, and 1.2.2 98releases — 1.2.1 fixed a bug that let anyone spend anyone's coins, and 1.2.2
diff --git a/docs/fair-launch.md b/docs/fair-launch.md
new file mode 100644
index 0000000..79eca8e
--- /dev/null
+++ b/docs/fair-launch.md
@@ -0,0 +1,69 @@
1# Bitflash fair-launch verifier
2
3Bitflash ships a deterministic fair-launch audit script:
4
5```bash
6python3 scripts/verify-fair-launch.py --max-blocks 1000 --out bitflash-fair-launch-report.json --json
7```
8
9The verifier reads `blk*.dat` directly from the local Bitflash data directory.
10It does not use Berkeley DB, does not call the node binary, and does not need
11network access.
12
13The report proves the launch baseline that can be checked from the chain:
14
15- the genesis block hash matches the source constant;
16- the genesis merkle root matches the source constant;
17- the genesis transaction is a single coinbase transaction;
18- the genesis output total is exactly `50.00000000 BTF`;
19- the scanned block range and mined coinbase total are reproducible.
20
21For machines that do not have a synced data directory yet, the source constants
22can be checked without disk blocks:
23
24```bash
25python3 scripts/verify-fair-launch.py --reconstruct-genesis
26```
27
28## Deterministic output
29
30Use `--json` or `--out` for canonical JSON. The JSON is written with sorted
31keys, fixed separators, ASCII escaping, and one trailing newline. It deliberately
32does not include local paths or wall-clock generation time, so two auditors
33scanning the same block range produce the same bytes.
34
35That makes the report shareable:
36
37```bash
38python3 scripts/verify-fair-launch.py --max-blocks 1000 --out report.json --json
39sha256sum report.json
40```
41
42If another auditor runs the same command against the same chain prefix, the
43`sha256sum` should match.
44
45## OpenTimestamps
46
47OpenTimestamps is optional and external. It anchors the report in Bitcoin
48without adding any bridge or dependency to Bitflash consensus.
49
50After generating a canonical JSON report:
51
52```bash
53ots stamp report.json
54ots verify report.json.ots
55```
56
57The `.ots` file proves that the exact report bytes existed no later than the
58Bitcoin block that confirms the timestamp.
59
60## What this does not claim
61
62This is not a retroactive genesis commitment. The Bitflash genesis block already
63exists, so adding a new on-chain commitment to genesis would create a different
64chain. This verifier instead audits the launch state that is already on-chain
65and makes the result reproducible and timestampable.
66
67It also does not prove real-world miner identity. It proves the genesis state,
68the absence of non-coinbase premine in genesis, and the deterministic coinbase
69totals over the scanned prefix.
diff --git a/scripts/verify-fair-launch.py b/scripts/verify-fair-launch.py
new file mode 100644
index 0000000..ce396fb
--- /dev/null
+++ b/scripts/verify-fair-launch.py
@@ -0,0 +1,452 @@
1#!/usr/bin/env python3
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
10import argparse
11import datetime as _dt
12import hashlib
13import json
14import os
15import struct
16import sys
17from pathlib import Path
18
19
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
298
299def build_report(blocks, source_mode):
300 genesis = blocks[0]
301 launch_outputs = genesis["transactions"][0]["vout"] if genesis["transactions"] else []
302 coinbase_outputs = []
303 total_mined = 0
304 non_coinbase_txs = 0
305
306 for height, block in enumerate(blocks):
307 if not block["transactions"]:
308 continue
309 cb = block["transactions"][0]
310 if is_coinbase(cb):
311 amount = sum(out["value_satoshis"] for out in cb["vout"])
312 total_mined += amount
313 coinbase_outputs.append({
314 "height": height,
315 "block_hash": block["hash"],
316 "coinbase_txid": cb["txid"],
317 "value_satoshis": amount,
318 "value_btf": format_money(amount),
319 "outputs": cb["vout"],
320 })
321 non_coinbase_txs += sum(1 for tx in block["transactions"] if not is_coinbase(tx))
322
323 checks = {
324 "genesis_hash_matches_source": genesis["hash"] == GENESIS_HASH,
325 "genesis_merkle_matches_source": genesis["merkle_root"] == GENESIS_MERKLE,
326 "computed_merkle_matches_header": genesis["computed_merkle_root"] == genesis["merkle_root"],
327 "genesis_has_single_transaction": genesis["tx_count"] == 1,
328 "genesis_transaction_is_coinbase": bool(genesis["transactions"] and is_coinbase(genesis["transactions"][0])),
329 "genesis_supply_is_50_btf": sum(out["value_satoshis"] for out in launch_outputs) == 50 * COIN,
330 }
331
332 report = {
333 "schema": "bitflash-fair-launch-report-1",
334 "deterministic": True,
335 "source_mode": source_mode,
336 "chain": {
337 "name": "Bitflash",
338 "ticker": "BTF",
339 "message_start_hex": MAGIC.hex(),
340 "coin": COIN,
341 },
342 "source_constants": {
343 "genesis_hash": GENESIS_HASH,
344 "genesis_merkle_root": GENESIS_MERKLE,
345 "genesis_time": GENESIS_TIME,
346 "genesis_time_utc": utc(GENESIS_TIME),
347 "genesis_bits": "0x%08x" % GENESIS_BITS,
348 "genesis_nonce": GENESIS_NONCE,
349 "genesis_message": GENESIS_TEXT,
350 },
351 "scanned": {
352 "block_count": len(blocks),
353 "first_height": 0,
354 "last_height": len(blocks) - 1,
355 "non_coinbase_transactions": non_coinbase_txs,
356 "total_coinbase_satoshis": total_mined,
357 "total_coinbase_btf": format_money(total_mined),
358 },
359 "genesis": {
360 "hash": genesis["hash"],
361 "merkle_root": genesis["merkle_root"],
362 "computed_merkle_root": genesis["computed_merkle_root"],
363 "time": genesis["time"],
364 "time_utc": genesis["time_utc"],
365 "bits": genesis["bits"],
366 "nonce": genesis["nonce"],
367 "tx_count": genesis["tx_count"],
368 "coinbase_txid": genesis["transactions"][0]["txid"] if genesis["transactions"] else "",
369 "outputs": launch_outputs,
370 },
371 "coinbase_outputs": coinbase_outputs,
372 "checks": checks,
373 "verdict": "FAIR_LAUNCH_BASELINE_VERIFIED" if all(checks.values()) else "CHECKS_FAILED",
374 "external_timestamping": {
375 "status": "not_embedded",
376 "recommended": "OpenTimestamps",
377 "instructions": [
378 "Save the canonical JSON report.",
379 "Run: ots stamp bitflash-fair-launch-report.json",
380 "Later verify with: ots verify bitflash-fair-launch-report.json.ots",
381 ],
382 },
383 }
384 report["attestation_payload_sha256"] = hashlib.sha256(canonical_bytes(report)).hexdigest()
385 return report
386
387
388def print_text(report):
389 checks = report["checks"]
390 print("Bitflash fair-launch verifier")
391 print("schema: %s" % report["schema"])
392 print("source: %s" % report["source_mode"])
393 print("attestation payload sha256: %s" % report["attestation_payload_sha256"])
394 print()
395 print("Genesis")
396 print(" hash: %s" % report["genesis"]["hash"])
397 print(" merkle: %s" % report["genesis"]["merkle_root"])
398 print(" time: %s" % report["genesis"]["time_utc"])
399 print(" txs: %d" % report["genesis"]["tx_count"])
400 print(" supply: %s BTF" % format_money(sum(o["value_satoshis"] for o in report["genesis"]["outputs"])))
401 print()
402 print("Scanned")
403 print(" blocks: %d" % report["scanned"]["block_count"])
404 print(" height: %d..%d" % (report["scanned"]["first_height"], report["scanned"]["last_height"]))
405 print(" minted: %s BTF" % report["scanned"]["total_coinbase_btf"])
406 print(" normal transactions in range: %d" % report["scanned"]["non_coinbase_transactions"])
407 print()
408 print("Checks")
409 for key in sorted(checks):
410 print(" %s %s" % ("OK " if checks[key] else "FAIL", key))
411 print()
412 print("Verdict: %s" % report["verdict"])
413 print()
414 print("OpenTimestamps")
415 print(" canonical JSON can be stamped later with: ots stamp <report.json>")
416
417
418def main(argv):
419 ap = argparse.ArgumentParser(description=__doc__)
420 ap.add_argument("--datadir", default=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")
422 ap.add_argument("--json", action="store_true", help="print canonical JSON")
423 ap.add_argument("--out", help="write canonical JSON to this path")
424 ap.add_argument("--reconstruct-genesis", action="store_true",
425 help="do not read disk; reconstruct only the genesis block from source constants")
426 args = ap.parse_args(argv)
427
428 if args.reconstruct_genesis:
429 blocks = [reconstructed_genesis()]
430 source_mode = "reconstructed-genesis"
431 datadir = ""
432 else:
433 datadir = Path(args.datadir)
434 blocks = read_blocks(datadir, args.max_blocks)
435 if not blocks:
436 raise SystemExit("no Bitflash blocks found in %s; use --reconstruct-genesis for a source-only check" % datadir)
437 source_mode = "local-block-files"
438
439 report = build_report(blocks, source_mode)
440 data = canonical_bytes(report)
441
442 if args.out:
443 Path(args.out).write_bytes(data)
444 if args.json:
445 sys.stdout.buffer.write(data)
446 else:
447 print_text(report)
448 return 0 if report["verdict"] == "FAIR_LAUNCH_BASELINE_VERIFIED" else 1
449
450
451if __name__ == "__main__":
452 raise SystemExit(main(sys.argv[1:]))