Пользовательские игры - Provably Fair
Как используется в пользовательских играх
Каждая ставка в YEET Originals начинается с раундового сида. Раундовый сид — это HMAC-SHA256 вашего клиентского сида и нонса ставки, зашифрованный серверным сидом, выбранным YEET. Затем игра извлекает каждое необходимое для этой ставки случайное значение из раундового сида. Серверный сид отображается вам в хэшированном виде до начала игры и раскрывается при смене пары сидов, поэтому вы можете пересчитать раундовый сид любой прошлой ставки и убедиться, что результат не был изменен после того, как вы сделали ставку.
Другие верификаторы могут не подойти
Сайты для проверки честности, созданные для других казино, могут генерировать случайные числа иначе, поэтому проверка ставки YEET на них может дать другой результат. Проверяйте ставки YEET во вкладке «Честность» (Fairness) в окне игры или с помощью кода на этой странице.
Генерация случайных чисел
Каждая ставка генерируется в два эташа. Во-первых, ваша пара сидов и нонс ставки дают один раундовый сид. Во-вторых, раундовый сид создает поток случайных байтов, из которого игра считывает непредвзятые целые числа. Одинаковые входные данные всегда дают одинаковые числа — именно это делает результат проверяемым.
1. Входные данные
- Случайная 64-символьная шестнадцатеричная строка, созданная нашим сервером
- Строка, которую вы можете изменить в любой момент
- Счетчик, который увеличивается с каждой сыгранной игрой
- Курсор — последняя часть сообщения раундового сида. Он всегда равен 0.
2. Раундовый сид
Раундовый сид — это HMAC-SHA256, зашифрованный серверным сидом (в виде 32 байт) поверх текста clientSeed:nonce:0. Результат составляет 32 байта, записанных как 64 шестнадцатеричных символа. Одна ставка имеет ровно один раундовый сид.
async function deriveGameSeed(serverSeed, clientSeed, nonce) {
const seedBytes = hexToBytes(serverSeed);
if (seedBytes.length !== 32) {
throw new Error(`Server seed must be 32 bytes (64 hex characters), got ${seedBytes.length}.`);
}
const key = await crypto.subtle.importKey(
'raw', toArrayBuffer(seedBytes), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
);
const message = new TextEncoder().encode(`${clientSeed}:${nonce}:0`);
return bytesToHex(new Uint8Array(await crypto.subtle.sign('HMAC', key, message)));
}3. Энтропия из раундового сида
Раундовый сид является ключом для второго HMAC-SHA256. Каждый вызов подписывает 8-байтовый счетчик (little-endian, начиная с 0) и выдает блок из 32 байт; счетчик увеличивается на единицу для каждого блока. Чтение берет байты из текущего блока по порядку. Когда для чтения требуется больше байтов, чем осталось в блоке, остаток блока пропускается и генерируется следующий блок.
class GameEntropy {
constructor(seedBytes) {
if (seedBytes.length !== 32) throw new Error('Seed must be exactly 32 bytes (64 hex characters).');
this.buffer = new Uint8Array(32);
this.offset = 32;
this.cursor = 0;
this.key = crypto.subtle.importKey(
'raw', toArrayBuffer(seedBytes), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
);
}
async next(count) {
if (count < 1 || count > 32) throw new RangeError('count must be between 1 and 32');
if (this.offset + count > 32) {
const message = new ArrayBuffer(8);
const view = new DataView(message);
view.setUint32(0, this.cursor >>> 0, true);
view.setUint32(4, Math.floor(this.cursor / 0x100000000) >>> 0, true);
const hash = await crypto.subtle.sign('HMAC', await this.key, message);
this.buffer.set(new Uint8Array(hash));
this.cursor++;
this.offset = 0;
}
const slice = this.buffer.slice(this.offset, this.offset + count);
this.offset += count;
return slice;
}
}4. Честные числа
Результаты определяются как целые числа. readUint32 считывает 4 байта как 32-битное целое число без знака (little-endian). unbiasedUint(entropy, n) возвращает число от 0 до n − 1, где у каждого одинаковый шанс на выпадение. Если n — степень двойки, берется просто остаток. В противном случае принимаются только значения ниже наибольшего кратного n, которое помещается в 32 бита; если значение больше или равно этому числу, происходит повторное считывание, чтобы исключить предвзятость исхода.
async function readUint32(entropy) {
const bytes = await entropy.next(4);
return new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, true);
}
async function unbiasedUint(entropy, range) {
if (range <= 0) throw new Error('Range must be positive');
if (range === 1) return 0;
if ((range & (range - 1)) === 0) {
return (await readUint32(entropy)) % range;
}
const maxValue = 0xffffffff;
const threshold = maxValue - (maxValue % range) - 1;
for (;;) {
const raw = await readUint32(entropy);
if (raw <= threshold) return raw % range;
}
}5. Помощники
function hexToBytes(hex) {
if (typeof hex !== 'string') throw new TypeError('Hex value must be a string.');
const clean = hex.trim().replace(/^0x/i, '');
if (clean.length % 2 !== 0) throw new Error('Hex value must have an even length.');
if (!/^[0-9a-fA-F]*$/.test(clean)) throw new Error('Hex value contains non-hex characters.');
const bytes = new Uint8Array(clean.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
}
return bytes;
}
function bytesToHex(bytes) {
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}
function toArrayBuffer(bytes) {
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
}6. Проверка ставки
- Откройте ставку в истории, скопируйте клиентский сид, nonce и хеш серверного сида.
- Смените пару сидов, чтобы увидеть серверный сид, и убедитесь, что его SHA-256 хеш совпадает с тем, что вы видели перед ставкой.
- Запустите
deriveGameSeedс серверным сидом, клиентским сидом и nonce, чтобы получить игровой сид раунда. - Создайте
GameEntropyиз сида раунда, примените конвертацию игры (см. раздел Конвертации) и сравните результат с вашей ставкой.
