In February 2024, we integrated a multi-party provably fair scheme that relied on ActuallyFair's Vx service. However, they have
announced that the service will shut down. For this reason, we are reverting to the classic scheme that only needs a hash and a salt to generate game results.
1. A chain of 100 million SHA256 hashes was generated in our
third seeding event. Each element is the hash of the binary value of the previous hash (i.e. the chain is used in reverse order). The hash of the chain's last element—the one we published back then—is 567a98370fb7545137ddb53687723cf0b8a1f5e93b1f76f4a1da29416930fa59. This remains unchanged, we simply pick up at the next unused hash.
You can verify that a hash belongs to our chain with the function below.
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
const TERMINATING_HASH = "567a98370fb7545137ddb53687723cf0b8a1f5e93b1f76f4a1da29416930fa59";
function verifyInChain(hash: Uint8Array) {
for (let gameId = 1; gameId < 100e6; gameId++) {
hash = sha256(hash);
if (bytesToHex(hash) === TERMINATING_HASH) {
console.log("hash is in the chain. It is game: ", gameId);
return gameId;
}
}
console.error("hash is not in the chain");
}
2. To prove we are not picking a chain that is favorable to the house, we will mix in the lowercase, hexadecimal representation of the hash of a Bitcoin block that has not been mined yet:
Bitcoin block 917000. Once that block is mined, we will post the block's hash here, and that will be our new game salt.
3. We take the next unused hash from the chain and use it to determine game results:
import { hmac } from "@noble/hashes/hmac";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex } from "@noble/hashes/utils";
export function gameResult(salt: Uint8Array, gameHash: Uint8Array) {
const nBits = 52; // number of most significant bits to use
// 1. HMAC_SHA256(key=salt, message=hash)
const hash = bytesToHex(hmac(sha256, salt, gameHash));
// 2. r = 52 most significant bits
const seed = hash.slice(0, nBits / 4);
const r = Number.parseInt(seed, 16);
// 3. X = r / 2^52
let X = r / Math.pow(2, nBits); // uniformly distributed in [0; 1)
// 4. X = 99 / (1 - X)
X = 99 / (1 - X); // 1 - X so there's no chance of div-by-zero
// 5. return max(trunc(X), 100)
const result = Math.floor(X);
return Math.max(1, result / 100);
}
For additional reference code, see our
open-source verifier, which will be updated to reflect these changes.