Solving the Trust Issue with Online Gambling
The underlying concept of provable fairness is that players have the ability to prove and verify that their results are fair and unmanipulated. This is achieved through the use of a commitment scheme , along with cryptographic hashing.
The commitment scheme is used to ensure that the player has an influence on all results generated. Cryptographic hashing is used to ensure that the casino also remains honest to this commitment scheme. Both concepts combined creates a trust-less environment when gambling online.
This is simplified in the following representation:
fair result = operators input (hashed) + customers input
3rd Party Verification
All Stake Originals played on Stake can be verified both here and via 3rd party websites who have also open sourced the verification procedure. You can find them via a google search, or simply check out some of these that have been put together by our community:
Crypto Gambling Foundation
Stake is a verified operator on the Crypto Gambling Foundation network. This foundation aims to uphold the highest standard of provably fair gambling and we are proud to be a part of their network. You can find further information and insights about provable fairness and the power it has in this industry, check out the Crypto Gambling Foundation via their website: cryptogambling.org
Random Number Generation
For each verifiable bet, a client seed, a server seed, a nonce and a cursor are used as the input parameters for the random number generation function. This function utilises the cryptographic hash function HMAC_SHA256 to generate bytes which are then used as the foundation for how we generate provably fair random outcomes on our platform.
// Random number generation based on following inputs: serverSeed, clientSeed, nonce and cursor
function byteGenerator({ serverSeed, clientSeed, nonce, cursor }) {
// Setup curser variables
let currentRound = Math.floor(cursor / 32);
let currentRoundCursor = cursor;
currentRoundCursor -= currentRound * 32;
// Generate outputs until cursor requirement fullfilled
while (true) {
// HMAC function used to output provided inputs into bytes
const hmac = createHmac('sha256', serverSeed);
hmac.update(`${clientSeed}:${nonce}:${currentRound}`);
const buffer = hmac.digest();
// Update curser for next iteration of loop
while (currentRoundCursor < 32) {
yield Number(buffer[currentRoundCursor]);
currentRoundCursor += 1;
}
currentRoundCursor = 0;
currentRound += 1;
}
}
Server Seed
The server seed is generated by our system as a random 64-character hex string. You are then provided with an encrypted hash of that generated server seed before you place any bets. The reason we provide you with the encrypted form of the server seed is to ensure that the un-hashed server seed cannot be changed by the casino operator, and that the player cannot calculate the results beforehand.
To reveal the server seed from its hashed version, the seed must be rotated by the player, which triggers the replacement with a newly generated one.
From this point you are able to verify that the hashed server seed matches that of the un-hashed server seed. This process can be verified via our un-hashed server seed function found in the menu above.
Client Seed
The client seed belongs to the player and is used to ensure they have influence on the randomness of the outcomes generated. Without this component of the algorithm, the server seed alone would have complete leverage over the outcome of each bet.
All players are free to edit and change their client seed regularly to create a new chain of random upcoming outcomes. This ensures the player has absolute control over the generation of the result, similar to cutting the deck at a brick and mortar casino.
During registration, a client seed is created for you by your browser, to ensure your initial experience with the site goes uninterrupted. Whilst this randomly generated client seed is considered suitable, we highly recommend that you choose your own, so that your influence is included in the randomness.
You can do this via the fairness modal.
Nonce
The nonce is simply a number that increments as every new bet is made. Due to the nature of the SHA256 cryptographic function, this creates a completely new result each time, without having to generate a new client seed and server seed.
The implementation of nonce, ensures we remain committed to your client seed and server seed pair, whilst generating new results for each bet placed.
Cursor (Incremental Number)
We use 4 bytes of data to generate a single game result, and because SHA256 is limited to 32 bytes, we utilise this implementation of a cursor to give us the ability to create more game events without having to modify our provable fair algorithm.
The cursor is only iterated over when the game being played requires the generation of more than 8 (32 bytes / 4 bytes) possible outcomes. For example: when we need to use more than 8 cards in a game of blackjack.
The cursor starts as 0 and gets increased by 1 every time the 32 bytes are returned by the HMAC_SHA256 function. If we don’t require more than 8 random numbers to be generated for the game events, then the curser does not increment as there is no need to generate any additional possible game outcomes.
Games with more than 1 incremental number:
- Hilo (Unlimited to cover required amount of cards)
- Keno (2 increments for every game due to 10 possible outcomes)
- Mines (3 increments per game for 24 possible bomb locations)
- Plinko (2 increments per game to cover possible 16 decisions)
- Blackjack (Unlimited to cover required amount of cards)
- Video Poker (7 increments to generate 52 possible cards in a full deck)
- Diamond Poker (2 increments to cover 10 diamonds: 5 per player/dealer)
- Slots (The incremental number is only utilised for bonus rounds)
Games with only 1 incremental number (represented as default value 0):
- Dice
- Limbo
- Wheel
- Baccarat
- Roulette
- Diamonds
Bytes to Floats
The output of the Random Number Generator (byteGenerator) function is a hexadecimal 32-byte hash. As explained under the cursor implementation, we use 4 bytes of data to generate a single game result. Each set of 4 bytes are used to generate floats between 0 and 1 (4 bytes are used instead of one to ensure a higher level of precision when generating the float.) It is with these generated floats that we derive the formal output of the provable fair algorithm before it is translated into game events.
// Convert the hash output from the rng byteGenerator to floats
function generateFloats ({ serverSeed, clientSeed, nonce, cursor, count }) {
// Random number generator function
const rng = byteGenerator({ serverSeed, clientSeed, nonce, cursor });
// Declare bytes as empty array
const bytes = [];
// Populate bytes array with sets of 4 from RNG output
while (bytes.length < count * 4) {
bytes.push(rng.next().value);
}
// Return bytes as floats using lodash reduce function
return _.chunk(bytes, 4).map(bytesChunk =>
bytesChunk.reduce((result, value, i) => {
const divider = 256 ** (i + 1);
const partialResult = value / divider;
return result + partialResult;
}, 0)
);
};
Floats to Game Events
Where the process of generating random outputs is universal for all our games, it’s at this point in the game outcome generation where a unique procedure is implemented to determine the translation from floats to game events.
The randomly float generated is multiplied by the possible remaining outcomes of the particular game being played. For example: In a game that uses a 52 card deck, this would simply be done by multiplying the float by 52. The result of this equation is then translated into a corresponding game event. For games where multiple game events are required, this process continues through each corresponding 4 bytes in the result chain that was generated using the described byteGenerator function.
Shuffle of Game Events
For games such as Keno, Mines and Video Poker, where outcomes cannot be duplicated, we then utilise the Fisher-Yates shuffle algorithm. This procedure influences the conversion process from floats to game events because each time a game event is translated, the amount of possible remaining game event possibilities has been reduced for any remaining steps in the result chain.
As an example, in video poker, there is at first 52 cards available in the complete deck, and therefore the first game event is translated by multiplying the float by 52. Once this card has been dealt, there is only 51 remaining cards in the deck, and therefore the second card translation is done by multiplying the second float generated by 51. This continues in the same fashion until all the game events required have been generated.
With regards to Mines and Keno, this is simply a matter of implementing the same process as explained with video poker but changing that to tiles or locations on the board or grid, ensuring that each game event generated, hasn’t already been done so beforehand in the chain of results.
Game events are translation of the randomly generated floats into a relatable outcome that is game specific. This includes anything from the outcome of a dice roll to the order of the cards in a deck, or even the location of every bomb in a game of mines.
Below is a detailed explanation as to how we translate floats into events for each particular different game on our platform.
Blackjack, Hilo & Baccarat
In a standard deck of cards, there are 52 unique possible outcomes. When it comes to playing Blackjack, Hilo & Baccarat on our platform, we utilise an unlimited amount of decks when generating the game event, and therefore each turn of a card always has the same probability. To calculate this, we multiply each randomly generated float by 52, and then translate that result into a particular card, based on the following index:
// Index of 0 to 51 : ♦2 to ♣A
const CARDS = [
♦2, ♥2, ♠2, ♣2, ♦3, ♥3, ♠3, ♣3, ♦4, ♥4,
♠4, ♣4, ♦5, ♥5, ♠5, ♣5, ♦6, ♥6, ♠6, ♣6,
♦7, ♥7, ♠7, ♣7, ♦8, ♥8, ♠8, ♣8, ♦9, ♥9,
♠9, ♣9, ♦10, ♥10, ♠10, ♣10, ♦J, ♥J, ♠J,
♣J, ♦Q, ♥Q, ♠Q, ♣Q, ♦K, ♥K, ♠K, ♣K, ♦A,
♥A, ♠A, ♣A
];
// Game event translation
const card = CARDS[Math.floor(float * 52)];
The only differentiating factor involved with these games is that with Hilo and Blackjack there is a curser of 13 to generate 52 possible game events for cases where a large amount of cards are required to be dealt to the player, whereas when it comes to Baccarat we only ever need 6 game events generated to cover the most amount of playable cards possible.
Diamond Poker
When playing Diamond Poker, there is 7 possible outcomes in the form of gems. To achieve this, we multiply each float generated by 7 before it is translated into a corresponding gem using the following index:
// Index of 0 to 6 : green to blue
const GEMS = [ green, purple, yellow, red, cyan, orange, blue ];
// Game event translation
const gem = GEMS[Math.floor(float * 7)];
Both the dealer and the player are dealt 5 gems each, which means that a complete game of Diamond Poker requires the generation of 10 game events. The first 5 are assigned to the dealer and the second 5 are assigned to the player.
Diamonds
When playing Diamonds, there is 7 possible outcomes in the form of gems. To achieve this, we multiply each float generated by 7 before it is translated into a corresponding gem using the following index:
// Index of 0 to 6 : green to blue
const GEMS = [ green, purple, yellow, red, cyan, orange, blue ];
// Game event translation
const gem = GEMS[Math.floor(float * 7)];
The player is then dealt 5 gems.
Dice Roll
In our version of dice, we cover a possible roll spread of 00.00 to 100.00, which has a range of 10,001 possible outcomes. The game event translation is done by multiplying the float by number of possible outcomes and then dividing by 100 so that the resulting number fits the constraints of our stated dice range.
// Game event translation
const roll = (float * 10001) / 100;
Limbo
When it comes to Limbo, we use a two-step process. Firstly, we take the float and multiply it by both the maximum possible multiplier and the house edge. Then, in order to generate a game event that has probability distribution , we divide the maximum possible multiplier by the result of the first step to create the game event in the form of a crash point.
// Game event translation with houseEdge of 0.99 (1%)
const floatPoint = 1e8 / (float * 1e8) * houseEdge;
// Crash point rounded down to required denominator
const crashPoint = Math.floor(floatPoint * 100) / 100;
// Consolidate all crash points below 1
const result = Math.max(crashPoint, 1);
Plinko
For any game of Plinko, the generated outcome is based on the path of the falling ball. The game event determines the direction of the falling ball for each level in the falling process. Players can choose between 8 and 16 pins of play, which determines the number of game events required to generate a complete path from top to bottom. Since there are only two possible directions (left or right) the translation is done by multiplying each float by 2, which maps to the following index:
// Index of 0 to 1 : left to right
const DIRECTIONS = [ left, right ];
// Game event translation
const direction = CARDS[Math.floor(float * 2)];
Roulette Roll
Our Roulette is derived from the European version of the game where the wheel consists of 37 possible different pockets, ranging from 0 to 36. The game event is calculated by multiplying the float by 37 and then translated into a corresponding pocket using the following index:
// Index of 0 to 36
const POCKETS = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
30, 31, 32, 33, 34, 35, 36
];
// Game event translation
const pocket = POCKETS[Math.floor(float * 37)];
Keno
Traditional Keno games require the selection of 10 possible game events in the form of hits on a board. To achieve this, we multiply each float by the number of possible unique squares that exist. Once a hit has been placed, it cannot be chosen again, which changes the pool size of the possible outcomes. This is done by subtracting the size of possible maximum outcomes by 1 for each iteration of game event result generated using the corresponding float provided, using the following index:
// Index of 0 to 39 : 1 to 40
const SQUARES = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40
];
const hit = SQUARES[Math.floor(float * 40)];
The fisher-yates shuffle implementation is utilised to prevent duplicate possible hits being generated.
Mines
A mine game is generated with 24 separate game events, in the form of mines on the board. Each float is multiplied by the number of possible unique tiles still remaining on the board. This is done by subtracting the number of tiles remaining by 1 for each iteration of game event result generated using the corresponding float provided. The location of the mine is plotted using a grid position from left to right, top to bottom.
The fisher-yates shuffle implementation is utilised to prevent duplicate possible hits being generated. Between 1 and 24 game event results are used, based on the settings chosen.
Video Poker
A video poker game involves 52 separate game events, in the form of cards in a deck. Each float is multiplied by the number of possible cards still remaining in the deck. This is done by subtracting the number of cards remaining by 1 for each iteration of game event result generated using the corresponding float provided. This is done by selecting a card from the following index:
// Index of 0 to 51 : ♦2 to ♣A
const CARDS = [
♦2, ♥2, ♠2, ♣2, ♦3, ♥3, ♠3, ♣3, ♦4, ♥4,
♠4, ♣4, ♦5, ♥5, ♠5, ♣5, ♦6, ♥6, ♠6, ♣6,
♦7, ♥7, ♠7, ♣7, ♦8, ♥8, ♠8, ♣8, ♦9, ♥9,
♠9, ♣9, ♦10, ♥10, ♠10, ♣10, ♦J, ♥J, ♠J,
♣J, ♦Q, ♥Q, ♠Q, ♣Q, ♦K, ♥K, ♠K, ♣K, ♦A,
♥A, ♠A, ♣A
];
// Game event translation
const card = CARDS[Math.floor(float * 52)];
The fisher-yates shuffle implementation is utilised to prevent duplicate cards being generated.
Wheel
The game event number is calculated by multiplying the float by the possible outcomes in the segment. It is then used to determine the game event result as a multiplier, using the following index:
// Index per payout configuration
const PAYOUTS = {
'10': {
low: [ 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0 ],
medium: [ 0, 1.9, 0, 1.5, 0, 2, 0, 1.5, 0, 3 ],
high: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 9.9 ]
},
'20': {
low: [
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0
],
medium: [
1.5, 0, 2, 0, 2, 0, 2, 0, 1.5, 0,
3, 0, 1.8, 0, 2, 0, 2, 0, 2, 0
],
high: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 19.8
]
},
'30': {
low: [
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0
],
medium: [
1.5, 0, 1.5, 0, 2, 0, 1.5, 0, 2, 0,
2, 0, 1.5, 0, 3, 0, 1.5, 0, 2, 0,
2, 0, 1.7, 0, 4, 0, 1.5, 0, 2, 0
],
high: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 29.7
]
},
'40': {
low: [
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0
],
medium: [
2, 0, 3, 0, 2, 0, 1.5, 0, 3, 0,
1.5, 0, 1.5, 0, 2, 0, 1.5, 0, 3, 0,
1.5, 0, 2, 0, 2, 0, 1.6, 0, 2, 0,
1.5, 0, 3, 0, 1.5, 0, 2, 0, 1.5, 0
],
high: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 39.6
]
},
'50': {
low: [
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0
],
medium: [
2, 0, 1.5, 0, 2, 0, 1.5, 0, 3, 0,
1.5, 0, 1.5, 0, 2, 0, 1.5, 0, 3, 0,
1.5, 0, 2, 0, 1.5, 0, 2, 0, 2, 0,
1.5, 0, 3, 0, 1.5, 0, 2, 0, 1.5, 0,
1.5, 0, 5, 0, 1.5, 0, 2, 0, 1.5, 0
],
high: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 49.5
]
}
};
// Game event translation
const spin = PAYOUTS[segments][risk][float * segments];
Crash
See the BitcoinTalk seeding thread to learn about how we utilise the salt hash based provable fairness modal for this particular game.
Slide
See the BitcoinTalk seeding thread to learn about how we utilise the salt hash based provable fairness modal for this particular game.
Scarab Spin / Tome of Life
The game event number is calculated by multiplying the float by the possible outcomes in the reel. The first 4 reels have a length of 30 possible outcomes, whilst the last reel has 41. The game event determines the central stop position for each reel. This game consists of 5 game event numbers, until the case of a bonus round, where more are generated.
Blue Samurai
Blue Samurai slots has 3 different types of spins. Regular, bonus and special.
For regular and bonus spins, 18 floats from 0 to 1 are generated from your hash. Unlike Scarab Spin slots, which has fixed reels, Samurai slots has dynamic reels, meaning each symbol is generated from the corresponding float that was assigned to it.
We use weighted random sampling to assign each float to its corresponding tile, in the same order, moving down the reels, from left to right. Each symbol has its own fixed probability / chance of appearing in any one tile, with the outer 2 reels having a different set of probabilities to the inner 3 reels. For a bit more information on how symbols are selected, see fitness proportionate selection algorithm to learn more.
Special spins are slightly different. For a start only 12 floats are taken from your hash, as the outer reels are disabled. Between each special spin, any samurai symbols stay in place for the remainder of the game, with the result being the final count of samurais. This means that if you were to have for example 1 samurai stick in the first spin – we’d technically only need 11 floats for the subsequent spin. For the sake of simplicity in the probably fair model, we just generate 12 floats every time, and if the float that was allocated for a tile has a stuck samurai from a previous spin, then that float is not used at all.
Dragon Tower
A Dragon Tower game is generated with 9 separate game events, in the form of the levels up the tower. We generate a number of eggs depending on the difficulty for each level, and have a range of tiles the egg can be on also represented by an integer.
Each float generated is then converted to integers to determine the egg location on each row. For example: A level on difficulty easy would be represented like this: [0, 1, 3] – eggs would be present at tile 1 and 2 and 4.
// count represents the number of eggs
// size represents the number of possible squares
const LEVEL_MAP = {
easy: { count: 3, size: 4 },
medium: { count: 2, size: 3 },
hard: { count: 1, size: 2 },
expert: { count1, size: 3 },
master: { count: 1, size: 4 },
}
The fisher-yates shuffle implementation is utilised to prevent duplicate eggs on a row.