diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-06 01:53:03 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-06 01:53:03 +0000 |
| commit | ef9f06d43fc6577c15df2256abbb25b20d4227b8 (patch) | |
| tree | 7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /scripts/verify-fair-launch.py | |
| parent | be63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff) | |
| parent | ceae3d076b5ad11ee84729ae43f14e9312896646 (diff) | |
Bring main onto the line this project actually ships (incl. the 1.2.23 compactblock port)
Closes #6, #7, #8, #11, #35, #34, #29, #28, #27, and #16
See merge request !16
Diffstat (limited to 'scripts/verify-fair-launch.py')
| -rw-r--r-- | scripts/verify-fair-launch.py | 166 |
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 | |||
| 4 | import argparse | ||
| 5 | import hashlib | ||
| 6 | import sys | ||
| 7 | from pathlib import Path | ||
| 8 | |||
| 9 | import bitflash_chain as chain | ||
| 10 | |||
| 11 | |||
| 12 | def 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 | |||
| 101 | def 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 | |||
| 131 | def 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 | |||
| 165 | if __name__ == "__main__": | ||
| 166 | raise SystemExit(main(sys.argv[1:])) | ||