Verifiably fair online gambling
An exciting rückenball match is coming up. The Schizos, your favorite team, are set to face off against The Glowies for the semi-final of some regional championship that’s undoubtedly quite a big deal. To make the evening more exciting you visit ballbets.com, in secret from your wife, and place a sizable bet that The Schizos will mop the stadium’s floor with their opponents.
In the example above, the outcome of the event being bet on is public knowledge. Everybody with internet (or newspaper) access can quickly verify which team won the match. The betting website therefore, has no means by which to cheat you. This betting scheme is implicitly trustworthy, because the gambling institution is not the one affecting the outcome of the event.
Online casinos on the other hand are a different story. When you bet on a number on a digital roulette, your bet is sent to the server who then spins the wheel and informs you of the outcome after a few moments of playful animations on your display. The fact of the matter here is that, there is no physical roulette being spun anywhere during this process. The platform’s backend simply generates a random number and pronounces the results based on that. Since we can’t audit the server code in real time as it is running, there is nothing preventing the system from simply not picking an outcome that crowns you winner.
The abovementioned problem is typically the subject of regulatory scrutiny. You are - in theory - only allowed to operate an establishment with games of chance, if you’ve allowed the government a peek at your source code and they’ve determined that your sources of randomness are fair and indiscriminate. Relying on regulation however is not a foolproof remedy.
- Auditors are human, and therefore fallible. Sometimes also corrupt.
- Regulation isn’t reliable everywhere. Maybe in the US online casinos are fair, but what if I’d like to play on one that is based in Mongolia?
- Complex systems are difficult to audit. Impossibly so when they are running in real time: it is completely opaque to the user whether the system taking his bet is the same one that was audited.
- Regulation is not free. It needs to be funded through your taxes, and it severely restricts your options when it comes to the quantity and quality of services that you may have access to.
In this short article, I’d like to propose a method which can make any online game provably fair.
Betting games
This chapter covers games where the player determines and places a specific bet. These are usually referred to as Table Games, or Live Games.
The high level picture of the problem is the following: a player sends a bet X
to a dealer, who then responds with outcome Y
.
%%{init: { 'sequence': {'mirrorActors':false} } }%% sequenceDiagram actor Player actor Dealer Player->>Dealer: bet X activate Dealer Note over Dealer: black box Dealer->>Player: outcome Y deactivate Dealer
The Player needs to:
- Be able to verify independently that the outcome
Y
was not influenced by his placing betX
. 1 - Not be able to predict the game’s outcome before placing his bet.
The solution
The solution to this problem is quite simple really: determine the outcome before the bet is even placed!
%%{init: { 'sequence': {'mirrorActors':false} } }%% sequenceDiagram actor Player actor Dealer Note over Dealer: S := random()<br>K = hash(S) Dealer->>Player: algorithm, K Player->>Dealer: bet X activate Dealer Note over Dealer: Y := algorithm(S) Dealer->>Player: Y, S deactivate Dealer activate Player Note over Player: K == hash(S)<br>Y == algorithm(S)
- For each game, before a bet is placed, the dealer generates a random seed
S
. - The dealer computes
K
, which is a hash ofS
. - The dealer communicates
K
to the player, while also sharing the algorithm that will be used to determine the outcome. - The player places bet
X
. - The dealer computes outcome
Y
by executing the algorithm, seeding it withS
. - The dealer sends both
Y
andS
to the user. - The user verifies the fairness of the game.
- Hashing
S
must produceK
. - Running
algorithm(S)
must produceY
.
- Hashing
Examples
Even though the solution presented above is incredibly simple, it is not immediately obvious how it can be applied to any game. Therefore I’d like to also include some examples of how this might be implemented for a select few cases.
Roulette
def algorithm_roulette(S: int) -> int:
random.seed(S)
return random.randint(0, 36)
Lottery
For a lottery, each number may be drawn only once. So we need to create a list with all the possible numbers, shuffle it, and pick out however many we need from the beginning.
def algorithm_lottery(S: int) -> list[int]:
"""6-from-49 lotto"""
all_balls = [num for num in range(1, 50)]
random.seed(S)
random.shuffle(all_balls)
return all_balls[:6]
Non-betting games
The other kind of games, are the ones where the player doesn’t bet on anything. Only taking the press of a button as input, the machine determines the game’s outcome.
%%{init: { 'sequence': {'mirrorActors':false} } }%% sequenceDiagram actor Player actor Dealer Player->>Dealer: activate Dealer Note over Dealer: black box Dealer->>Player: outcome Y deactivate Dealer
The Player needs to:
- Be able to verify independently that the outcome
Y
was fairly sampled from a random distribution. - Not be able to predict the game’s outcome before initiating it.
Solution
The problem here is slightly different, and the previous solution for betting games does not apply.
Instead, what is needed here is for the player to influence the seed.
%%{init: { 'sequence': {'mirrorActors':false} } }%% sequenceDiagram actor Player actor Dealer Note over Dealer: Sd := random()<br>K = hash(Sd) Dealer->>Player: algorithm, K Note over Player: Sp := random() Player->>Dealer: Sp activate Dealer Note over Dealer: Y := algorithm(Sd + Sp) Dealer->>Player: Y, Sd deactivate Dealer activate Player Note over Player: K == hash(Sd)<br>Y == algorithm(Sd + Sp)
With this method, all important properties hold:
- The dealer cannot selectively choose a biased
Sd
that would result in the player losing the game. - The player cannot selectively choose an
Sp
that would result in him winning the game. - The player can verify that the game was fair.
Examples
Slot machine
def algorithm_slot_machine(Sd: int, Sp: int) -> tuple[int, int, int]:
"""3-reel slot machine with 7 icons on each reel"""
random.seed(Sd + Sp)
a = random.randint(1, 7)
b = random.randint(1, 7)
c = random.randint(1, 7)
return a, b, c
Blackjack
Any game that is played with a deck of cards can utilize the same algorithm. All the fairness we need is in shuffling the deck, just like when playing in real life!
def algorithm_deck(Sd: int, Sp: int) -> Generator[int, None, None]:
deck = [card for card in range(52)]
random.seed(Sd + Sp)
random.shuffle(deck)
for i in range(52):
# Draw cards one at a time, as needed for the game.
yield deck[i]
Note that the seed Sd
here is only shared with the player after the conclusion of the game.
Competitive games
The final case, are games where the Player competes against other players in a game of chance, with the Dealer acting as intermediary. Typically the dealer executes some black box random process, and declares the winner among the participants.
%%{init: { 'sequence': {'mirrorActors':false} } }%% sequenceDiagram actor Player1 actor Player2 actor Player3 actor Dealer Player1->>Dealer: Player2->>Dealer: Player3->>Dealer: activate Dealer Note over Dealer: black box Dealer->>Player1: win deactivate Dealer Dealer->>Player2: lose Dealer->>Player3: lose
The Puppet Master
problem
Whenever analyzing competitive games of chance, it is necessary to constantly keep one thing in mind: from the Player’s perspective, any of the other players can bots controlled by the dealer. Proving that all participants in the game are human (and not affiliated with the casino) is neither feasible, nor should ever be required. First and foremost, because there is absolutely no reason why these game’s shouldn’t be played anonymously.
So when designing such games to be verifiably fair, we need to take this into account. Essentially we need to treat the player from whose perspective we are trying to validate the fairness of the game, as the only player. All other players including the Dealer, might as well be treated as a single adversarial entity. And we shall assume that they share all information, and have a common objective: beating the Player.
Solution
Here we need to extend the solution for non-betting games: each player must come up with his own seed, and share it with the dealer and all other participants.
Due to the Puppet Master
problem however, there is one necessary step that must precede the sharing of seeds.
First, each player must send a hash of his seed to all other players.
And only after every participant has received each other participant’s hashed seed, should the actual seeds be transmitted.
This is done to ensure that all players generate their seed fairly, otherwise a puppet player who already knows all other seeds would be able to come up with one that ensures a favorable outcome.
%%{init: { 'sequence': {'mirrorActors':false} } }%% sequenceDiagram actor Player1 actor Player2 actor Player3 actor Dealer Note over Dealer: Sd := random()<br>Kd = hash(Sd) Dealer->>Player1: algorithm, Kd Dealer->>Player2: algorithm, Kd Dealer->>Player3: algorithm, Kd Note over Player1: S1 := random()<br>K1 = hash(S1) Player1->>Player2: K1 Player1->>Player3: K1 Note over Player2: S2 := random()<br>K2 = hash(S2) Player2->>Player1: K2 Player2->>Player3: K2 Note over Player3: S3 := random()<br>K3 = hash(S3) Player3->>Player1: K3 Player3->>Player2: K3 Note over Player1, Player3: Hashes sent Player1->>Player2: S1 Player1->>Player3: S1 Player1->>Dealer: S1 Player2->>Player1: S2 Player2->>Player3: S2 Player2->>Dealer: S2 Player3->>Player1: S3 Player3->>Player2: S3 Player3->>Dealer: S3 Note over Player1, Player3: Seeds sent activate Dealer Note over Dealer: Y := algorithm(Sd + S1 + S2 + S3) Dealer->>Player1: Y, Sd deactivate Dealer Dealer->>Player2: Y, Sd Dealer->>Player3: Y, Sd
Examples
Raffle
With the method provided here, running a fair raffle is trivial. Each contestant simply needs to provide their own seed when entering, as shown above.
def algorithm_raffle(Sd: int, Sn: list[int]) -> int:
random.seed(Sd + sum(Sn))
# Pick one of the contestants as the winner.
num_players = len(Sn)
return random.randint(0, num_players - 1)
Poker
A game like Poker is more intricate.
Due to the Puppet Master
problem that described above, we need to assume that the other players in the game are colluding with the Dealer.
And because the dealer holds all seeds (Sd, S1, .., Sn
) they can all predict all the cards that will be drawn next.
This is not a problem during the serving of the initial hands, because the Dealer commits to an Sd
first, and all the players get locked in to the game simultaneously upon providing their own seeds.
So each human player that participates makes the initial draws unpredictable by contributing his own seed.
After the initial draw however, players colluding with the Dealer can use their knowledge of future card draws to make decisions during the game, such as when to call or fold.
The solution is again quite simple: all players must provide a new seed each time new cards need to be drawn from the deck.
class Deck:
def __init__(self, Sd: int):
self.sd = Sd
self.deck = [card for card in range(52)]
def draw_cards(self, Sn: list[int], num_cards: int) -> list[int]:
# Shuffle the deck each time we are about to draw cards from it.
random.seed(self.sd + sum(Sn))
random.shuffle(self.deck)
# Draw the first N cards.
return [self.deck.pop(0) for n in range(num_cards)]
General solution
The progression from betting games, to non-betting games, and finally to competitive games in this article was deliberate. My intent was to familiarize the reader with the problem, by starting from the simplest case and then adding constraints that appear in the games of chance we play today.
Having covered everything however, we can now put together the general form of the solution which can take any game of chance involving a Dealer and one or more Players, and make it verifiably fair by all participants.
Before any randomized algorithm is to be executed in a game of chance:
- The Dealer generates a random secret seed
Sd
.- The Dealer computes
Kd := hash(Sd)
and transmits it to all players.- Each Player (
i
):
- Generates a random secred seed
Si
.- Computes
Ki := hash(Si)
and transmits it to all other players.- Each Player, after receiving
Ki
from all other players:
- Transmis
Si
to all other players, and to the Dealer.- The Dealer computes the combined seed as
Sc := Sd + S0 + S1 + ... + Sn
.- The Dealer executes the algorithm, seeding it with
Sc
:Y := algorithm(Sc)
- The Dealer transmits the outcome of the algorithm
Y
to all players, together withSd
.- Each Player may independently verify that the game was fair, if:
- All
Ki
received during step 3, equalhash(Si)
for allSi
received in step 4.- The outcome
Y
equals what is obtained by executingalgorithm(Sd + S0 + S1 + ... + Sn)
.
Implementation notes
Committing to the game
Everything outlined here has a small but important implication: all players, including the dealer, must commit to the game before the dealer transmits his hashed seed. In other words, players should have their credits held on the server side as soon as they choose to initiate the game. Refusal to participate in the game after that moment in time should forfeit their credits 2.
If there is no penalty for quiting a game, then players who collude with the dealer could choose to exit games after seeds have been chosen and it is clear that they will be dealt a losing hand.
Salting
In the interest of brevity, I neglected to mention a miniscule vulnerability in the solutions discussed above: the same seed will always produce the same hash. Though astronomically unlikely, this would in theory allow someone to predict the outcome of a game if they happen to figure out the seeds.
This vulnerability can be trivially mitigated, by having the dealer generate a random salt D
, hashing it together with his seed K = hash(Sd + D)
and then transmitting D
to the players as well. Of course, each player who contributes a seed could potentially do the same during the hash-sharing step.
Conclusion
Other alternatives do exist, like Provably fair, that guarantee the game’s fairness either by adding the player’s input to the seed, or employing other complicated mechanisms where seeds are derived from blockchain data.
The system offered here is so simple, it can even be verified with pen and paper in some cases.
Therefore it is unclear to me why regulation is needed in any case. Any game can be transparent and fair by design, can it not?
-
The actual odds of outcome
Y
do not interest us here. The dealer’s system will always be a black box. Whether or not the outcomes follow a prescribed probability distribution is something that is game-dependant and will have to be checked statistically by observing a large number of outcomes. ↩︎ -
In the case of raffles they can just be excluded after paying the entry fee. Or in the case of poker, for example, it can be treated as folding. ↩︎