#!/usr/bin/env python3 """DEGENT independent fairness verifier — no trust in the server required. Usage: python3 verify.py [base_url] Recomputes the committed shuffle for a finished hand and checks: 1. the revealed seed matches the pre-deal commitment (SHA-256) 2. the board cards match the deterministic deck 3. every showdown-revealed hand matches its dealt position 4. the record's position in the tamper-evident hash chain Deck derivation (also documented at /docs.html#fairness): stream = HMAC-SHA256(key=seed, msg=uint64_be(counter)) for counter 0,1,2... concatenated, consumed 4 bytes at a time as big-endian uint32 deck = [0..51], card index = (rank-2)*4 + suit, suits ordered c,d,h,s shuffle = Fisher-Yates from the top: for i = 51..1, j = uniform [0,i] via rejection sampling (values >= floor(2^32/n)*n discarded) dealing = two cards per seat in listed seat order, then the board, no burns """ import hashlib import hmac import json import struct import sys import urllib.request RANKS = "23456789TJQKA" SUITS = "cdhs" def card_name(i): return RANKS[i // 4] + SUITS[i % 4] def fair_deck(seed: bytes): buf = b"" counter = 0 def next32(): nonlocal buf, counter while len(buf) < 4: buf += hmac.new(seed, struct.pack(">Q", counter), hashlib.sha256).digest() counter += 1 v = struct.unpack(">I", buf[:4])[0] buf = buf[4:] return v def intn(n): limit = (1 << 32) // n * n while True: v = next32() if v < limit: return v % n deck = list(range(52)) for i in range(51, 0, -1): j = intn(i + 1) deck[i], deck[j] = deck[j], deck[i] return [card_name(c) for c in deck] def main(): if len(sys.argv) < 2: sys.exit(__doc__) hand_no = int(sys.argv[1]) base = sys.argv[2] if len(sys.argv) > 2 else "https://degent.xyz" hand = json.load(urllib.request.urlopen(f"{base}/api/hands/{hand_no}"))["hand"] if not hand.get("seed"): sys.exit("hand not finished: seed not yet revealed") seed = bytes.fromhex(hand["seed"]) ok = True # 1. commitment commit_ok = hashlib.sha256(seed).hexdigest() == hand["commitment"] print(f"commitment matches seed: {commit_ok}") ok &= commit_ok # 2 + 3. re-deal and compare deck = fair_deck(seed) pos, holes = 0, {} for s in hand["seats"]: holes[s["seat"]] = [deck[pos], deck[pos + 1]] pos += 2 board_ok = all(deck[pos + i] == c for i, c in enumerate(hand.get("board") or [])) print(f"board matches deck: {board_ok}") ok &= board_ok reveals_ok = all(holes.get(r["seat"]) == r["cards"] for r in hand.get("reveals") or []) print(f"showdown reveals match: {reveals_ok}") ok &= reveals_ok # 4. hash chain link (tamper-evidence across history) if hand.get("hash"): record = {k: v for k, v in hand.items() if k != "hash"} canonical = json.dumps(record, separators=(",", ":"), sort_keys=True).encode() chain_ok = hashlib.sha256(canonical).hexdigest() == hand["hash"] print(f"hash-chain entry valid: {chain_ok}") if hand.get("prev_hash"): print(f" chained to previous hand: {hand['prev_hash'][:16]}…") ok &= chain_ok else: print("hash-chain entry valid: (hand predates chaining)") print("\nVERDICT:", "FAIR ✓" if ok else "MISMATCH ✗") sys.exit(0 if ok else 1) if __name__ == "__main__": main()