#!/usr/bin/env python3 """ Dalanga Transparency Chain Verification Tool A standalone, zero-dependency Python script that verifies the integrity of a downloaded Dalanga system chain export file. Requirements: Python 3.6+ (standard library only). Usage: python3 verify-chain.py Examples: python3 verify-chain.py system-chain-export.json python3 verify-chain.py /tmp/chain-export.json What it verifies: 1. Consecutive sequence numbers starting from 0 2. Hash linking: each block's previous_hash matches the preceding block's hash 3. Hash recomputation: re-hashes each block's hash_input and compares to stored hash 4. Genesis block validation: sequence 0 must have previous_hash = null 5. Payload hash verification: re-hashes payload and compares to stored payload_hash 6. Draw fairness: for draw.revealed events, re-derives the winner using the algorithm_version from event_data and confirms the recorded winner matches Exit codes: 0 = Chain is valid (all checks passed) 1 = Chain is invalid (one or more checks failed) 2 = Usage error (missing file, invalid JSON, etc.) """ import hashlib import json import sys # ANSI color codes GREEN = "\033[32m" RED = "\033[31m" RESET = "\033[0m" BOLD = "\033[1m" def success(message: str) -> None: print(f"{GREEN}\u2713{RESET} {message}") def failure(message: str) -> None: print(f"{RED}\u2717{RESET} {message}") # --- Input Validation ------------------------------------------------------- if len(sys.argv) < 2: print("Dalanga Transparency Chain Verification Tool") print() print(f"Usage: python3 {sys.argv[0]} ") print() print("Verifies the cryptographic integrity of a chain export file.") print("For draw events, re-derives the winner to confirm fairness.") sys.exit(2) path = sys.argv[1] try: with open(path, "r", encoding="utf-8") as f: content = f.read() except FileNotFoundError: failure(f"File not found: {path}") sys.exit(2) except OSError as e: failure(f"Cannot read file: {e}") sys.exit(2) try: data = json.loads(content) except json.JSONDecodeError as e: failure(f"Invalid JSON: {e}") sys.exit(2) if not isinstance(data.get("blocks"), list): failure('Invalid export format: missing "blocks" array') sys.exit(2) # --- Chain Verification ------------------------------------------------------ blocks = data["blocks"] block_count = len(blocks) errors = 0 print(f"{BOLD}Dalanga Transparency Chain Verifier{RESET}") if "chain" in data: print(f"Chain: {data['chain']}") if "exported_at" in data: print(f"Exported: {data['exported_at']}") print(f"Blocks: {block_count}") print() if block_count == 0: success("Chain is empty (0 blocks)") sys.exit(0) # Chain integrity checks for i in range(block_count): block = blocks[i] # Check consecutive sequences if block["sequence"] != i: failure(f"Block {i}: expected sequence {i}, got {block['sequence']}") errors += 1 continue # Genesis block validation if i == 0 and block["previous_hash"] is not None: failure("Block 0: genesis block must have previous_hash = null") errors += 1 # Hash linking if i > 0 and block["previous_hash"] != blocks[i - 1]["hash"]: failure(f"Block {i}: previous_hash does not match preceding block's hash") errors += 1 # Hash recomputation recomputed_hash = hashlib.sha256(block["hash_input"].encode("utf-8")).hexdigest() if block["hash"] != recomputed_hash: failure(f"Block {i}: hash does not match recomputed hash from hash_input") errors += 1 else: event_type = block.get("event_type", "unknown") success(f"Block {i}: hash valid ({event_type})") # --- Payload Hash Verification ----------------------------------------------- has_payload_hashes = any("payload_hash" in b for b in blocks) if has_payload_hashes: print() print(f"{BOLD}Payload Hash Verification{RESET}") payload_checked = 0 payload_skipped = 0 for block in blocks: if "payload_hash" not in block: continue if "payload" not in block: payload_skipped += 1 continue payload_checked += 1 payload_json = json.dumps(block["payload"], ensure_ascii=False, separators=(",", ":"), sort_keys=True) recomputed_payload_hash = hashlib.sha256(payload_json.encode("utf-8")).hexdigest() if block["payload_hash"] != recomputed_payload_hash: failure(f"Block {block['sequence']}: payload_hash does not match recomputed hash from payload") errors += 1 else: success(f"Block {block['sequence']}: payload hash valid ({block['event_type']})") if payload_skipped > 0: print(f"(Skipped {payload_skipped} block(s) without payload data \u2014 blocks-only export)") print(f"Payload hashes checked: {payload_checked}, skipped: {payload_skipped}") # --- Draw Event Verification ------------------------------------------------- committed_blocks = [b for b in blocks if b.get("event_type") == "draw.committed"] for block in blocks: if block.get("event_type") != "draw.revealed": continue event_data = block.get("event_data", {}) algorithm_version = event_data.get("algorithm_version") if algorithm_version is None: failure(f"Block {block['sequence']}: draw.revealed missing algorithm_version") errors += 1 continue if algorithm_version != "sha256-modulo-v1": failure(f"Block {block['sequence']}: unknown algorithm version '{algorithm_version}'") errors += 1 continue # Find matching draw.committed block committed = None for cb in committed_blocks: cb_data = cb.get("event_data", {}) if "eligible_tickets_snapshot" in cb_data: committed = cb if committed is None: failure(f"Block {block['sequence']}: draw.revealed but no matching draw.committed found") errors += 1 continue committed_data = committed["event_data"] seed = event_data["seed"] eligible_tickets = committed_data["eligible_tickets_snapshot"] total_eligible = len(eligible_tickets) # Re-derive winner using sha256-modulo-v1 winner_index = int(hashlib.sha256(seed.encode("utf-8")).hexdigest()[:12], 16) % total_eligible winner_ticket_number = eligible_tickets[winner_index] if winner_index != event_data["winner_index"]: failure(f"Block {block['sequence']}: draw winner_index mismatch (expected {winner_index}, got {event_data['winner_index']})") errors += 1 elif winner_ticket_number != event_data["winner_ticket_number"]: failure(f"Block {block['sequence']}: draw winner_ticket_number mismatch (expected {winner_ticket_number}, got {event_data['winner_ticket_number']})") errors += 1 else: success(f"Block {block['sequence']}: draw winner independently verified (sha256-modulo-v1)") # Verify commitment hash commitment_hash = committed_data.get("commitment_hash") if commitment_hash is not None: recomputed_commitment = hashlib.sha256( (seed + json.dumps(eligible_tickets, separators=(",", ":"))).encode("utf-8") ).hexdigest() if commitment_hash != recomputed_commitment: failure(f"Block {committed['sequence']}: commitment hash mismatch") errors += 1 else: success(f"Block {committed['sequence']}: commitment hash verified") # --- Summary ----------------------------------------------------------------- print() if errors == 0: print(f"{GREEN}Chain valid: {block_count} blocks verified, 0 errors{RESET}") sys.exit(0) print(f"{RED}Chain INVALID: {block_count} blocks verified, {errors} error(s){RESET}") sys.exit(1)