PROVABLY FAIR
What does it mean?
The outcome of each lottery pool is calculated in a deterministic way given a random secret, the hash of a target Bitcoin block, and an optional user-provided seed. A cryptographic hash of the random secret is shown at the beginning of each lottery pool before any stakes are made. By publishing the hash of the secret, we are verifying that the secret is not changed after stakes are made. By also incorporating the hash of a future Bitcoin block that is unknowable at the beginning of the lottery and a user-provided seed, we are ensuring that it is not possible for us to choose the secret in such a way as to influence the outcome of the lottery.
ALGORITHM
At the beginning of each lottery, a new 32-byte secret is randomly chosen. The secret is kept private until the lottery is complete, but its hash is calculated using the BLAKE2b hash function with a digest size of 256 bits and shown to the user before any stakes are made.
When placing a stake, the user can provide an arbitrary string seed. The MD5 hash of this seed (or of an empty string if no seed is provided) will be used in the selection of prize winners.
After the target Bitcoin block is mined and its hash is known, the lottery prizes are calculated and awarded to users who have placed stakes with following algorithm.
First, the jackpot outcome is determined by taking the 256 bit BLAKE2b hash of the secret concatenated with the block hash and converting the first 8 bytes of that hash to a floating point number between 0 and 1. If this number is less than the jackpot probability, the jackpot will be the first prize awarded with a prize id of 0, otherwise, no jackpot is awarded. The actual jackpot amount is determined by multiplying the total jackpot pool by the fraction calculated by dividing by the floating point jackpot outcome.
Prizes are then awarded to users by first getting a unique ordering of all stakes by sorting by their seed hash and user_id. A prize outcome is then calculated by taking the 256 bit BLAKE2b hash of the secret concatenated with the block hash and prize id and converting the first 8 bytes to a floating point number between 0 and 1. The prize outcome is multiplied by the total amount staked and the first user whose cumulative sorted stake amount exceeds this number is selected as the prize winner. This stake is then removed from consideration for any additional pool prizes. This process is repeated for all prizes or until no stakes remain.
VERIFICATION EXAMPLE
After the outcome of a lottery pool has been revealed, the results can be verified with the following python code. It includes the calculation of the hashes for user-defined seeds for 'feet' and an empty seed, the hash of the secret, the jackpot outcome and amount, and the selection of stakes for periodic prizes:
import hashlib
import math
import pyblake2
jackpot_fraction = 0.01
jackpot_probability = 0.10
jackpot_pool = 100000.00
target_block = b'723059'
block_hash = b'000000000000000000050c9b466a268896cf0cff13536a21e84b617647e16deb'
secret = b'8094e4063e56f0d2b3fdb3f2c2161bfc'
prizes = [
{'prize_id': b'0', 'prize': 'Jackpot'},
{'prize_id': b'1', 'prize': '$900'},
{'prize_id': b'2', 'prize': '$500'},
{'prize_id': b'3', 'prize': '15000 RLB'}
]
stakes = [
{
'user_id': b'6208a625c8faf3a391602992',
'seed_hash': b'e03f9063484fb1967d1675c86a6094d7',
'staked': 120000
},
{
'user_id': b'6208a625c8faf3a391602dfc',
'seed_hash': b'd41d8cd98f00b204e9800998ecf8427e',
'staked': 56000
},
{
'user_id': b'6208a625c8faf3a3916029b6',
'seed_hash': b'f1425da40a9f2d21ab702a1c7feae026',
'staked': 12000
},
{
'user_id': b'6208a625c8faf3a39160295c',
'seed_hash': b'd41d8cd98f00b204e9800998ecf8427e',
'staked': 24500
},
{
'user_id': b'6208a625c8faf3a391602d10',
'seed_hash': b'ee3b5077c54ebc131320fd2448d3733b',
'staked': 63565
}
]
print('seed_hash for 1st stake:', hashlib.md5(b'feet').hexdigest())
print('seed_hash for 2nd stake:', hashlib.md5(b'').hexdigest())
print('secret_hash:', pyblake2.blake2b(secret, digest_size=32).digest().hex())
def bytes_to_uniform_number(xs: bytes) -> float:
hash = pyblake2.blake2b(xs, digest_size=32).digest()
return int.from_bytes(hash[:8], byteorder='little', signed=False) / float(2**64 - 1)
jackpot_outcome = bytes_to_uniform_number(secret + block_hash)
jackpot_won = jackpot_outcome < jackpot_probability
print('Jackpot won:', jackpot_won)
if jackpot_won:
jackpot_amount = math.floor(100.0 * jackpot_pool * min(1.0, jackpot_fraction * jackpot_probability / jackpot_outcome)) / 100.0
print('Jackpot amount:', jackpot_amount)
else:
prizes.pop(0)
for stake in stakes:
stake['staked'] = round(100.0 * (stake['staked'] / 100.0))
stakes.sort(key=lambda x: (x['seed_hash'], x['user_id']))
for prize in prizes:
if len(stakes) == 0:
break
total_staked = sum([x['staked'] for x in stakes])
outcome = bytes_to_uniform_number(secret + block_hash + prize['prize_id'])
outcome_stake = math.floor(outcome * total_staked)
current_stake = 0
for stake in stakes:
current_stake += stake['staked']
if outcome_stake < current_stake:
print(f"User {stake['user_id'].decode('ascii')} won prize id {prize['prize_id'].decode('ascii')} ({prize['prize']})")
stakes = [x for x in stakes if x['user_id'] != stake['user_id']]
break
OUTPUT
seed_hash for 1st stake: e03f9063484fb1967d1675c86a6094d7
seed_hash for 2nd stake: d41d8cd98f00b204e9800998ecf8427e
secret_hash: 6a20b23ff5186c8c5cdfbe1c413f64f1178c3a60f8e59035ce7d88f5778d3dcb
Jackpot won: True
Jackpot amount: 3246.91
User 6208a625c8faf3a391602d10 won prize id 0 (Jackpot)
User 6208a625c8faf3a391602dfc won prize id 1 ($900)
User 6208a625c8faf3a3916029b6 won prize id 2 ($500)
User 6208a625c8faf3a391602992 won prize id 3 (15000 RLB)
SECRET HASH
ENTRIES