Seeds to bytes
Bytes to numbers
Numbers to result
Provable fair
The fundamental concept of provable fairness is that players have the ability to prove and confirm that the results of their games are fair and have not been manipulated by the site. This is achieved through the use of the so-called commitment scheme, where random game outcomes are generated based on a set of parameters, some of which are known to the player before the start of the round. The commitment scheme is used to ensure that the player influences all received outcomes, while the parameters are chosen in such a way that the player cannot predict the outcome of any game in advance, but can always deterministically confirm the fairness of the game.
RNG parameters
In the proposed system as the input parameters of the random number generator, the following set of four parameters is used
The server seed is generated by our system and consists of 32 bytes represented as a 64-character string. For modes where only one player participates in one round, the player has the opportunity to request the system to generate a new server seed, the new value of which will be immediately displayed. These modes include:
- Mines
- Dice
- Tower
- Slot
- Plinko
For modes where multiple players participate in one round, a single player cannot request the system to generate a new server seed (this is because the system is unclear which player's request to satisfy), however, the fairness guarantee in these modes is that the server seed is constant, meaning it does not change from round to round. A pre-calculated hash of a specific bitcoin block is taken as the value of the server seed. The modes and their corresponding constant server seeds are provided below:
- Double - block #500000: blockchain.com/btc/block/500000
- Jackpot - block #520000: blockchain.com/btc/block/520000
- Crash - block #540000: blockchain.com/btc/block/540000
- x50 - block #560000: blockchain.com/btc/block/560000
The client seed belongs to the player and is used to allow the player to influence the generated numbers. Initially, the client seed is generated by the system when registering on the website, however, at any time, the user can change their client seed (note that, unlike the server seed, it is possible to enter the value of the desired client seed). For modes where only one player participates in one round, the user's client seed is used as the client seed. These modes are listed below:
- Mines
- Dice
- Tower
- Plinko
For modes where multiple players participate in one round, a user's custom client seed cannot be used as the client seed (this is because the system is unclear which player's seed to use), however, the fairness guarantee in these modes is that the client seed is constant, meaning it does not change from round to round. A pre-calculated hash of a specific bitcoin block is taken as the value of the client seed. The modes and their corresponding constant client seeds are provided below:
- Double - block #510000: blockchain.com/btc/block/510000
- Jackpot - block #530000: blockchain.com/btc/block/530000
- Crash - block #550000: blockchain.com/btc/block/550000
- x50 - block #570000: blockchain.com/btc/block/570000
Since the server seed and client seed are constant and known to the user, the salt parameter is used in the scheme to obtain different results with the same pair of client seed + server seed. The salt is a random 32-byte value represented as a 64-character string. This parameter is unique for each round and is not available to the player before the start of the game, but is displayed at the end of the game (the so-called commitment unveiling phase in the commitment scheme).
The random number generator, using the server seed, client seed, and salt as input parameters, generates random 64 bytes represented as a 128-character string. This string is used to generate 8 random numbers. However, some modes require generating more numbers. For this purpose, an additional parameter called cursor is introduced, which is initially set to 0 and is incremented by one each time it is necessary to generate the next 8 numbers within one round. Modes that use more than one cursor:
- Mines (3 cursors)
- Tower (5 cursors)
- Slot (from 1 to 180 cursors)
- Plinko (from 2 to 200 cursors)
Games that use exactly one cursor:
- Dice
- Double
- x50
- Jackpot
- Crash
Random number generator
The generation of random bytes based on the input parameters is carried out using functions HMAC-SHA512 и SHA256
bytes = HMAC-SHA512(SHA256(server_seed:salt), client_seed:cursor)
The result of the generation is random 64 bytes, which are then divided into 8 blocks of 8 bytes, and each block is used to generate a random number in the range [0, 1). The algorithm for converting the block in the number is given in the section "Implementation", and is also graphically demonstrated when checking the game.
Test battery Dieharder
The Dieharder Test Battery is a set of statistical tests for measuring the quality of a random number generator. It consists of a multitude of tests (including the Diehard test suite and the NIST Test Suite), which are collectively considered one of the most stringent test suites available. We generated 1 billion random numbers using the aforementioned generator and tested this set using Dieharder with the following options
dieharder -g 202 -f ./rng_output_test_random_CS -a |& tee -a output_dh_random_CS.txt
For greater statistical significance, we conducted the test twice on different sets. The test results show that the provided random number generator indeed generates unbiased random sequences without statistically significant patterns. Reports in the form of text files are available at the following links:
output_dh_random_CS_1.txt
output_dh_random_CS_2.txt
Generation of random numbers
For any round, client seed, server seed, salt, and cursor serve as the sole input parameters for the generator. The generator transforms these parameters into a sequence of random bytes using the HMAC-SHA512 and SHA256 hash functions, utilizing the property of the avalanche effect.
function seedsToBytes(serverSeed, clientSeed, salt, cursor) {
// SHA-256 calculation
const hmacKey = createHash(serverSeed + ":" + salt);
const hmacMessage = clientSeed + ":" + cursor;
// HMAC calculation
return createHmac(hmacKey, hmacMessage);
}
Bytes to numbers
The result of the generator is 64 random bytes (128-symbolic line), which are divided into 8 blocks of 8 byte each, and each block is used to obtain one number in the interval [0, 1) using the algorithm below.
function numbersFromBytes(bytes) {
for (let t = 0; t < 8; t++) {
let value = 0;
let pw = 256;
for (let i = t * 8; i < t * 8 + 8; i++) {
value += bytes[i] / pw;
pw *= 256;
}
result.push(value);
}
return result;
}
Outcome derival in Mines
To generate the result in the Mines mode, 24 random numbers in the interval [0..1) are required, therefore 3 arrays with 8 numbers each are generated, which are then combined into one array called index. This array is used to select bomb positions on the field using the Fisher-Yates shuffle
// numbers - index array
function minesShuffling(numbers, minesCount) {
// Initial array
let range = [];
// Resulting array
let permutation = [];
for (var i = 0; i <= 24; i++) range.push(i);
for (var i = 0; i < numbers.length; i++) {
let index = Math.floor(numbers[i] * range.length);
permutation.push(range.splice(index, 1)[0]);
}
return permutation;
}
Outcome derival in Tower
To generate the result in Tower mode, 40 random numbers in the interval [0..1) are required, therefore 5 arrays with 8 numbers each are generated, which are then combined into one array called index. Then this array is divided into 10 subarrays of 4 elements, and each subarray is used to generate rearrangements in the corresponding row of the tower using the Fisher-Yates shuffle
// numbers - index array
function minesShuffling(numbers, minesCount) {
let field = [];
for (var i = 0; i < 10; i++) {
// Initial array
let range = [];
// Resulting array
let permutation = [];
for (var j = 0; j <= 4; j++) range.push(j);
for (var j = 0; j < 4; j++) {
permutation.push(range.splice(numbers[i * 4 + j], 1)[0]);
}
field.push(permutation);
}
return field;
}
Outcome derival in Slot
To generate one rotation in Slot mode, 5 random numbers are required in the interval [0..1), therefore, 1 array of 8 numbers is generated, of which the first 5 numbers are taken, which are converted into rotation of each drum by multiplication by its length according to the next algorithm
// numbers - index array
function slotSpin(numbers) {
let spin_positions = [];
for (var i = 0; i < 5; i++) {
spin_positions.push(Math.floor(numbers[i] * (i == 4 ? 41 : 30)))
}
return spin_positions;
}
Array spin_positions
contains the positions of symbols in the central row, the correspondence table of the numbers with the symbols is shown on the page "Check the game". The remaining characters on the drum are determined by the neighbors of the central characters in their columns. If the central symbol is the first (or last) in the table column, its upper (or lower) neighbor is the last (or first) symbol from this column.
Outcome derival in Plinko
To generate the winning bin number in Plinko mode, depending on the number of pins, from 8 to 16 random numbers in the interval [0..1) are required, so 2 arrays of 8 numbers are generated, which are then combined into one. The first n
numbers, where n
is the number of pins, are taken from this array. The resulting array is transformed as follows: if the array element is greater than 0.5
, it is replaced with 1
, otherwise it is replaced with 0
. The winning bin number is equal to the sum of all elements of the transformed array.
// numbers - array of n elements in [0..1)
function plinkoBucket(numbers) {
return numbers.map((number) => Math.floor(number * 2)).reduce((acc, item) => acc + item, 0);
}
The resulting value belongs to binomial distribution, which is a mathematical model of the Galton board.
Outcome derival in Dice
To generate the result in Dice mode, 1 random number in the interval [0..1) is required, which is then transformed to the outcome in the interval [0..100] according to the algorithm below.
// number has interval [0..1)
function diceOutcome(number) {
return Math.floor(number * 10001) / 100;
}
Outcome derival in Double
To generate the result in Double mode requires 1 random number in the interval [0..1), which is then transformed to the wheel sector in the interval [0..14] according to the algorithm below.
// number has interval [0..1)
function doubleOutcome(number) {
return Math.floor(number * 15);
}
Outcome derival in Jackpot
To generate the result in the Jackpot mode, 1 random number in the interval [0..1) is required, which is then transformed to a winning ticket by arithmetic multiplication by the total number of tickets rounded down, adjusted per unit (since the numbering of tickets begins with 1).
// number has interval [0..1)
function jackpotOutcome(number, ticketsCount) {
return Math.floor(number * ticketsCount) + 1;
}
Outcome derival in Crash
To generate the result in Crash mode, 1 random number in the interval [0..1) is required, which is then transformed to the Crash multiplier, which has an exponential distribution, according to the algorithm below.
// number has interval [0..1)
function crashOutcome(number) {
return max(1, 1000000 / (Math.floor(number * 1000000) + 1) * (1 - 0.05)).toFixed(2);
}
// number has interval [0..1)
function overgoOutcome(number) {
return max(1, 1000000 / (Math.floor(number * 1000000) + 1) * (1 - 0.03)).toFixed(2);
}
Outcome derival in x50
To generate the result in X50 mode, 1 random number in the interval [0..1) is required, which is then converted to the wheel sector in the interval [0..53] according to the algorithm below.
// number has interval [0..1)
function doubleOutcome(number) {
return Math.floor(number * 54);
}