Last active
March 4, 2022 02:16
-
-
Save 4Kaylum/6bbb3dcd9ebb0545ba8e692391d5fedd to your computer and use it in GitHub Desktop.
Implementation of the Primedice provably fair algorithm (https://dicesites.com/primedice/verifier)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import random | |
import secrets | |
import hashlib | |
import hmac | |
class ProvablyFair(random.Random): | |
"""An object that represents a provably fair algorithm""" | |
def __init__(self, client_seed:str=None, *, server_seed:str=None): | |
self.valid = True | |
self.nonce = -1 | |
self.client_seed = client_seed if client_seed else secrets.token_hex(10) | |
if not server_seed: | |
data = self.generate_server_seed() | |
else: | |
data = self._hash_server_seed(server_seed) | |
self._server_seed, self.server_seed_hash = data | |
self.last_rolled_data = None | |
@classmethod | |
def verify_roll(cls, server_seed, server_seed_hash, client_seed, nonce, roll): | |
"""Verifies that a roll passed successfully | |
Returns True if all was well, otherwise raising AssertionError""" | |
instance = cls(client_seed, server_seed=server_seed) | |
instance_roll = instance.random(nonce=nonce) | |
instance_data = instance.last_rolled_data | |
assert instance._server_seed == server_seed | |
assert instance.server_seed_hash == server_seed_hash | |
assert instance_data['nonce'] == nonce | |
assert instance_data['roll'] == roll | |
return True | |
@staticmethod | |
def generate_server_seed(): | |
"""Generates a new server seed to use for future rolls""" | |
server_seed = secrets.token_hex(20) | |
server_seed_hash_object = hashlib.sha256(server_seed.encode()) | |
server_seed_hash = server_seed_hash_object.hexdigest() | |
return server_seed, server_seed_hash | |
@staticmethod | |
def _hash_server_seed(server_seed): | |
"""Hashes and stores a given server seed""" | |
server_seed_hash_object = hashlib.sha256(server_seed.encode()) | |
server_seed_hash = server_seed_hash_object.hexdigest() | |
return server_seed, server_seed_hash | |
def invalidate(self): | |
"""Marks this instance as invalid and returns the server seed to you""" | |
self.valid = False | |
return self._server_seed | |
def random(self, *, nonce=None): | |
"""Generates a provably random number given a client seed and a | |
pre-generated server seed hash""" | |
# Make sure it's still valid | |
if not self.valid: | |
raise Exception('This server seed is no longer valid.') # TODO provide custom exception | |
# Get a nonce to use | |
if nonce: | |
self.nonce = nonce | |
else: | |
self.nonce += 1 | |
# HMAC it up | |
msg_str = '{}-{}'.format(self.client_seed, self.nonce) | |
key = self._server_seed.encode() | |
msg = msg_str.encode() | |
dig = hmac.new(key, msg=msg, digestmod=hashlib.sha512) | |
# Get the number | |
full_number = dig.hexdigest() | |
counter = 0 | |
while True: | |
number_str = full_number[counter:counter+5] | |
number = int(number_str, 16) | |
if number > 999_999: | |
counter += 5 | |
else: | |
break | |
# Return results | |
roll = (number % (10**4)) / 100 | |
self.last_rolled_data = { | |
'roll': roll, | |
'nonce': self.nonce, | |
'server_seed_hash': self.server_seed_hash, | |
'client_seed': self.client_seed | |
} | |
return roll / 100 | |
def seed(self, *args, **kwds): | |
"""Stub method. Not used for provably fair systems, since the seed is passed on initialization""" | |
return None | |
def getstate(self): | |
return self.nonce, self.client_seed, self._server_seed | |
def setstate(self, state): | |
self.nonce, self.client_seed, self._server_seed = state | |
def _randbelow(self, n): | |
return int(self.random() * n) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
sick