#!/usr/bin/env python3 """ Minestakes Provably Fair Verification Script Independently verify game outcomes using only Python 3 standard library. Usage: python verify.py crash [house_edge_pct] python verify.py mines [grid_size] [num_mines] python verify.py fishing python verify.py roulette """ import hmac import hashlib import struct import sys def server_seed_hash(seed: str) -> str: """SHA-256 hex digest of the server seed.""" return hashlib.sha256(seed.encode()).hexdigest() def outcome_hash(server_seed: str, client_seed: str, nonce: int) -> bytes: """HMAC-SHA256(server_seed, client_seed + ':' + nonce) -> 32 bytes.""" message = f"{client_seed}:{nonce}" return hmac.new(server_seed.encode(), message.encode(), hashlib.sha256).digest() def derive_mine_positions(hash_bytes: bytes, grid_size: int, num_mines: int) -> list: """ Fisher-Yates shuffle with 2-byte swaps and SHA-256 hash chaining. Returns exactly num_mines distinct cell indices in [0, grid_size*grid_size). """ total = grid_size * grid_size if num_mines >= total: return list(range(total)) indices = list(range(total)) current_hash = bytearray(hash_bytes) byte_offset = 0 for i in range(num_mines): if byte_offset + 2 > 32: current_hash = bytearray(hashlib.sha256(bytes(current_hash)).digest()) byte_offset = 0 r = struct.unpack(">H", bytes(current_hash[byte_offset:byte_offset + 2]))[0] byte_offset += 2 remaining = total - i swap_with = i + (r % remaining) indices[i], indices[swap_with] = indices[swap_with], indices[i] return indices[:num_mines] def derive_crash_point(hash_bytes: bytes, house_edge_pct: float = 0.0) -> float: """ Standard crypto casino crash point derivation. First 4 bytes as u32 big-endian. If h % 33 == 0 -> 1.0x (instant crash). Otherwise: crash_point = 2^32 / (2^32 - h) * (1 - house_edge_pct/100). Floored to 2 decimal places, minimum 1.00. """ h = struct.unpack(">I", hash_bytes[:4])[0] if h % 33 == 0: return 1.0 e = 2**32 raw = e / (e - h) adjusted = raw * (1.0 - house_edge_pct / 100.0) result = int(adjusted * 100) / 100.0 return max(result, 1.0) def derive_fishing_catch(hash_bytes: bytes, tier_thresholds: list = None, max_multiplier: float = None) -> dict: """ Derive fishing catch from outcome hash. Bytes 0-3: tier selection (uniform [0,1) roll). Bytes 4-7: multiplier interpolation within tier range. tier_thresholds: list of (cumulative_threshold, mult_min, mult_max). Returns dict with tier_index and multiplier. """ if tier_thresholds is None: tier_thresholds = [ (0.55, 0.0, 0.0), # Junk (55%) (0.80, 1.05, 1.35), # Common (25%) (0.93, 1.50, 2.50), # Uncommon (13%) (0.99, 2.00, 4.00), # Rare (6%) (1.00, 10.00, 36.00), # Legendary (1%) ] h0 = struct.unpack(">I", hash_bytes[:4])[0] roll = h0 / (0xFFFFFFFF + 1.0) h1 = struct.unpack(">I", hash_bytes[4:8])[0] interp = h1 / (0xFFFFFFFF + 1.0) tier_index = 0 mult_min = 0.0 mult_max = 0.0 for i, (threshold, mn, mx) in enumerate(tier_thresholds): if roll < threshold: tier_index = i mult_min = mn mult_max = mx break if mult_min == 0.0 and mult_max == 0.0: raw_multiplier = 0.0 else: raw_multiplier = mult_min + interp * (mult_max - mult_min) if max_multiplier is not None: raw_multiplier = min(raw_multiplier, max_multiplier) multiplier = int(raw_multiplier * 100) / 100.0 return {"tier_index": tier_index, "multiplier": multiplier} ROULETTE_GREENS = {15, 60, 116} ROULETTE_REDS = set() ROULETTE_BLACKS = set() # Build red/black sets matching the game logic for n in range(121): if n in ROULETTE_GREENS: continue if n % 2 == 1: ROULETTE_REDS.add(n) else: ROULETTE_BLACKS.add(n) def derive_roulette_number(hash_bytes: bytes) -> dict: """ First 4 bytes as u32 % 121 -> winning number (0-120). Returns dict with number and color. """ h = struct.unpack(">I", hash_bytes[:4])[0] number = h % 121 if number in ROULETTE_GREENS: color = "Green" elif number in ROULETTE_REDS: color = "Red" else: color = "Black" return {"number": number, "color": color} TIER_NAMES = ["Junk", "Common", "Uncommon", "Rare", "Legendary"] def main(): if len(sys.argv) < 5: print(__doc__.strip()) sys.exit(1) game = sys.argv[1].lower() s_seed = sys.argv[2] c_seed = sys.argv[3] nonce = int(sys.argv[4]) h = outcome_hash(s_seed, c_seed, nonce) print(f"Server Seed Hash: {server_seed_hash(s_seed)}") print(f"Outcome Hash: {h.hex()}") print() if game == "crash": edge = float(sys.argv[5]) if len(sys.argv) > 5 else 0.0 point = derive_crash_point(h, edge) print(f"Crash Point: {point:.2f}x") if edge > 0: print(f" (house edge: {edge}%)") elif game == "mines": grid = int(sys.argv[5]) if len(sys.argv) > 5 else 5 mines = int(sys.argv[6]) if len(sys.argv) > 6 else 3 positions = derive_mine_positions(h, grid, mines) print(f"Grid: {grid}x{grid}, Mines: {mines}") print(f"Mine positions: {sorted(positions)}") print() for row in range(grid): line = "" for col in range(grid): idx = row * grid + col line += " X" if idx in positions else " ." print(line) elif game == "fishing": result = derive_fishing_catch(h, max_multiplier=100.0) tier = TIER_NAMES[result["tier_index"]] if result["tier_index"] < len(TIER_NAMES) else f"Tier {result['tier_index']}" print(f"Tier: {tier}") print(f"Multiplier: {result['multiplier']:.2f}x") elif game == "roulette": result = derive_roulette_number(h) print(f"Number: {result['number']}") print(f"Color: {result['color']}") else: print(f"Unknown game: {game}") print("Supported: crash, mines, fishing, roulette") sys.exit(1) if __name__ == "__main__": main()