upleb.uk

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

summaryrefslogtreecommitdiff
path: root/scripts/verify-fair-launch.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/verify-fair-launch.py')
-rw-r--r--scripts/verify-fair-launch.py166
1 files changed, 166 insertions, 0 deletions
diff --git a/scripts/verify-fair-launch.py b/scripts/verify-fair-launch.py
new file mode 100644
index 0000000..c33c8be
--- /dev/null
+++ b/scripts/verify-fair-launch.py
@@ -0,0 +1,166 @@
1#!/usr/bin/env python3
2"""Produce a deterministic Bitflash fair-launch audit report."""
3
4import argparse
5import hashlib
6import sys
7from pathlib import Path
8
9import bitflash_chain as chain
10
11
12def build_report(blocks, source_mode):
13 genesis = blocks[0]
14 launch_outputs = genesis["transactions"][0]["vout"] if genesis["transactions"] else []
15 coinbase_outputs = []
16 total_mined = 0
17 non_coinbase_txs = 0
18
19 for height, block in enumerate(blocks):
20 if not block["transactions"]:
21 continue
22 cb = block["transactions"][0]
23 if chain.is_coinbase(cb):
24 amount = sum(out["value_satoshis"] for out in cb["vout"])
25 total_mined += amount
26 coinbase_outputs.append({
27 "height": height,
28 "block_hash": block["hash"],
29 "coinbase_txid": cb["txid"],
30 "value_satoshis": amount,
31 "value_btf": chain.format_money(amount),
32 "outputs": cb["vout"],
33 })
34 non_coinbase_txs += sum(1 for tx in block["transactions"] if not chain.is_coinbase(tx))
35
36 checks = {
37 "genesis_hash_matches_source": genesis["hash"] == chain.GENESIS_HASH,
38 "genesis_merkle_matches_source": genesis["merkle_root"] == chain.GENESIS_MERKLE,
39 "computed_merkle_matches_header": genesis["computed_merkle_root"] == genesis["merkle_root"],
40 "genesis_has_single_transaction": genesis["tx_count"] == 1,
41 "genesis_transaction_is_coinbase": bool(genesis["transactions"] and chain.is_coinbase(genesis["transactions"][0])),
42 "genesis_supply_is_50_btf": sum(out["value_satoshis"] for out in launch_outputs) == 50 * chain.COIN,
43 }
44
45 report = {
46 "schema": "bitflash-fair-launch-report-1",
47 "deterministic": True,
48 "source_mode": source_mode,
49 "chain": {
50 "name": "Bitflash",
51 "ticker": "BTF",
52 "message_start_hex": chain.MAGIC.hex(),
53 "coin": chain.COIN,
54 },
55 "source_constants": {
56 "genesis_hash": chain.GENESIS_HASH,
57 "genesis_merkle_root": chain.GENESIS_MERKLE,
58 "genesis_time": chain.GENESIS_TIME,
59 "genesis_time_utc": chain.utc(chain.GENESIS_TIME),
60 "genesis_bits": "0x%08x" % chain.GENESIS_BITS,
61 "genesis_nonce": chain.GENESIS_NONCE,
62 "genesis_message": chain.GENESIS_TEXT,
63 },
64 "scanned": {
65 "block_count": len(blocks),
66 "first_height": 0,
67 "last_height": len(blocks) - 1,
68 "non_coinbase_transactions": non_coinbase_txs,
69 "total_coinbase_satoshis": total_mined,
70 "total_coinbase_btf": chain.format_money(total_mined),
71 },
72 "genesis": {
73 "hash": genesis["hash"],
74 "merkle_root": genesis["merkle_root"],
75 "computed_merkle_root": genesis["computed_merkle_root"],
76 "time": genesis["time"],
77 "time_utc": genesis["time_utc"],
78 "bits": genesis["bits"],
79 "nonce": genesis["nonce"],
80 "tx_count": genesis["tx_count"],
81 "coinbase_txid": genesis["transactions"][0]["txid"] if genesis["transactions"] else "",
82 "outputs": launch_outputs,
83 },
84 "coinbase_outputs": coinbase_outputs,
85 "checks": checks,
86 "verdict": "FAIR_LAUNCH_BASELINE_VERIFIED" if all(checks.values()) else "CHECKS_FAILED",
87 "external_timestamping": {
88 "status": "not_embedded",
89 "recommended": "OpenTimestamps",
90 "instructions": [
91 "Save the canonical JSON report.",
92 "Run: ots stamp bitflash-fair-launch-report.json",
93 "Later verify with: ots verify bitflash-fair-launch-report.json.ots",
94 ],
95 },
96 }
97 report["attestation_payload_sha256"] = hashlib.sha256(chain.canonical_bytes(report)).hexdigest()
98 return report
99
100
101def print_text(report):
102 checks = report["checks"]
103 print("Bitflash fair-launch verifier")
104 print("schema: %s" % report["schema"])
105 print("source: %s" % report["source_mode"])
106 print("attestation payload sha256: %s" % report["attestation_payload_sha256"])
107 print()
108 print("Genesis")
109 print(" hash: %s" % report["genesis"]["hash"])
110 print(" merkle: %s" % report["genesis"]["merkle_root"])
111 print(" time: %s" % report["genesis"]["time_utc"])
112 print(" txs: %d" % report["genesis"]["tx_count"])
113 print(" supply: %s BTF" % chain.format_money(sum(o["value_satoshis"] for o in report["genesis"]["outputs"])))
114 print()
115 print("Scanned")
116 print(" blocks: %d" % report["scanned"]["block_count"])
117 print(" height: %d..%d" % (report["scanned"]["first_height"], report["scanned"]["last_height"]))
118 print(" minted: %s BTF" % report["scanned"]["total_coinbase_btf"])
119 print(" normal transactions in range: %d" % report["scanned"]["non_coinbase_transactions"])
120 print()
121 print("Checks")
122 for key in sorted(checks):
123 print(" %s %s" % ("OK " if checks[key] else "FAIL", key))
124 print()
125 print("Verdict: %s" % report["verdict"])
126 print()
127 print("OpenTimestamps")
128 print(" canonical JSON can be stamped later with: ots stamp <report.json>")
129
130
131def main(argv):
132 ap = argparse.ArgumentParser(description=__doc__)
133 ap.add_argument("--datadir", default=chain.default_datadir(), help="Bitflash data directory")
134 ap.add_argument("--max-blocks", type=int, default=100, help="number of blocks to scan from blk*.dat")
135 ap.add_argument("--json", action="store_true", help="print canonical JSON")
136 ap.add_argument("--out", help="write canonical JSON to this path")
137 ap.add_argument("--reconstruct-genesis", action="store_true",
138 help="do not read disk; reconstruct only the genesis block from source constants")
139 args = ap.parse_args(argv)
140
141 if args.reconstruct_genesis:
142 blocks = [chain.reconstructed_genesis()]
143 source_mode = "reconstructed-genesis"
144 else:
145 blocks = chain.read_blocks(args.datadir, args.max_blocks)
146 if not blocks:
147 raise SystemExit("no Bitflash blocks found in %s; use --reconstruct-genesis for a source-only check" % args.datadir)
148 source_mode = "local-block-files"
149
150 try:
151 report = build_report(blocks, source_mode)
152 except chain.ParseError as e:
153 raise SystemExit(str(e))
154 data = chain.canonical_bytes(report)
155
156 if args.out:
157 Path(args.out).write_bytes(data)
158 if args.json:
159 sys.stdout.buffer.write(data)
160 else:
161 print_text(report)
162 return 0 if report["verdict"] == "FAIR_LAUNCH_BASELINE_VERIFIED" else 1
163
164
165if __name__ == "__main__":
166 raise SystemExit(main(sys.argv[1:]))