Последнее обновление:
RaseBox использует модель проверяемой честности для кейсов, бесплатных кейсов, битв и улучшений. Каждый случайный результат детерминированно вычисляется с помощью HMAC-SHA256 из секретного начального значения сервера, начального значения клиента, nonce и контекста, который определяет игру. Перед игрой мы сохраняем хэш каждого начального значения сервера и передаём его вам как обязательство: для кейсов, бесплатных кейсов и улучшений он отправляется в ваш браузер, который его сохраняет, а для битв публикуется в манифесте битвы. Кейсы, бесплатные кейсы и улучшения в каждой игре используют новое одноразовое начальное значение сервера и новые случайные данные вашего браузера, поэтому nonce всегда равен 0: он не нужен, чтобы отличать игры друг от друга. В битвах nonce равен индексу раунда и начинается с 0 в первом раунде. Записи кейсов, бесплатных кейсов и улучшений, созданные до введения предварительных обязательств в сентябре 2026 года, показывают счётчик игр аккаунта, который тогда использовался как nonce; старые битвы описаны ниже.
Кейсы: у каждого предмета есть взвешенная вероятность. Сгенерированное число сопоставляется с этой таблицей, чтобы выбрать выигранный предмет. Порядок предметов в анимации перемешивается независимыми случайными данными сервера при каждой загрузке страницы, сохраняется для следующих прокруток на этой странице и записывается вместе с каждым открытием для повтора. Он не выводится ни из одного начального значения и не влияет ни на результат, ни на взвешенную таблицу, которая для проверки сохраняет свой исходный порядок.
Битвы: при создании битвы для каждого раунда и места создаётся отдельное начальное значение сервера, а в режимах с джекпотом — ещё одно для джекпота; их хэши публикуются в манифесте битвы. Каждый игрок заранее фиксирует новые случайные данные своего браузера, список участников замораживается, после чего эти вклады раскрываются и объединяются в одно общее начальное значение клиента битвы для всех мест; боты не добавляют собственных случайных данных. Все открытия генерируются и сохраняются до воспроизведения. Каждый результат привязан к идентификатору битвы, раунду, месту и кейсу, а его nonce равен индексу раунда (0 в первом раунде). Предмет выбирается по той же взвешенной таблице, что и при обычном открытии. Анимация лишь показывает сохранённый результат, а не определяет его. В начале битвы каждый участник, включая ботов, также получает независимый случайный порядок предметов в анимации для каждого раунда. Он записывается в план анимации битвы, защищённый собственным хэшем этого плана, и используется при воспроизведении в реальном времени и в повторах, не влияя ни на один результат. Битвы, созданные до введения предварительных обязательств, использовали начальное значение клиента каждого игрока, а в качестве nonce — его счётчик игр плюс индекс раунда; боты использовали хэш отдельно созданного начального значения и в качестве nonce — только индекс раунда.
Победители определяются по этим проверяемым значениям предметов: Normal и Безумие сравнивают наибольшую и наименьшую сумму соответственно; Финальный и Минимальный финал — только наибольшее и наименьшее значение последнего раунда, суммарное для команд; Группа делит результат поровну. Джекпот и Безумный джекпот добавляют отдельный розыгрыш HMAC с собственным заранее зафиксированным начальным значением сервера, тем же начальным значением клиента битвы и nonce, равным 0. Вес пропорционален сумме игрока или команды для Джекпот и обратно пропорционален ей для Безумный джекпот.
Улучшения: вероятность рассчитывается по вашему вкладу и стоимости цели. Если сгенерированное число ниже этого порога, улучшение успешно; иначе оно проигрывает.
Во время битвы хэши начальных значений сервера видны, а сами значения скрыты. После завершения адрес /api/battles/{battleId}/verify раскрывает начальные значения, значения nonce, контексты, случайные числа и зафиксированные таблицы предметов, необходимые для независимого пересчёта каждого открытия, суммы, победителя, розыгрыша джекпота и выплаты.
Каждое платное или бесплатное открытие использует отдельный серверный seed. Браузер сохраняет его хеш до генерации новых случайных данных клиента. Проверка сравнивает раскрытый результат с этой предварительной записью; старые открытия без такой записи не доказывают предварительную фиксацию.
1import crypto from 'node:crypto'
2
3// Protocol v1: paid cases AND free reward cases.
4// 1. On page load: POST /api/fairness/commitments with
5// { purpose: 'box' or 'free-box', batchId: a fresh identifier }.
6// The response contains 4 independent SHA-256 commitments, not seeds.
7// Each expires after 15 minutes. Save them in the browser before play.
8// 2. Only AFTER saving a hash, the browser samples a fresh 32-byte
9// clientSeed with crypto.getRandomValues. One commitment = one draw.
10// 3. POST the opening with fairness: { commitmentId, serverSeedHash,
11// clientSeed }. Keep the whole request + idempotency key for retries.
12// 4. The server consumes that seed in the SAME transaction as the game.
13// It cannot consume the same commitment again. No new seed is picked
14// while resolving the opening. nonce is fixed at 0 for this protocol.
15// 5. After the result, verify the revealed seed against your saved hash.
16// The next draw uses another seed; prefetch avoids an extra click delay.
17
18const TOTAL = 1_000_000n
19const BOUND = 2n ** 64n - (2n ** 64n % TOTAL)
20const sha256 = value => crypto.createHash('sha256').update(value).digest('hex')
21
22function deriveRoll({ serverSeed, clientSeed, nonce, context }) {
23 for (let counter = 0; counter < 2048; counter++) {
24 const digest = crypto.createHmac('sha256', serverSeed)
25 .update(`${clientSeed}:${nonce}:${context}:${counter}`).digest()
26 const random64 = digest.readBigUInt64BE(0)
27 // Reject the incomplete final interval to avoid modulo bias.
28 if (random64 < BOUND) return Number(random64 % TOTAL)
29 }
30 throw new Error('Unable to derive an unbiased roll')
31}
32
33function verifyCase({ savedReceipt, opening, selectionTable }) {
34 // savedReceipt is the browser's earlier record, not a hash first
35 // obtained alongside this result. Historical openings lack this proof.
36 if (!savedReceipt || !opening.commitment) return false
37 if (opening.commitment.id !== savedReceipt.id ||
38 opening.serverSeedHash !== savedReceipt.serverSeedHash ||
39 sha256(opening.serverSeed) !== savedReceipt.serverSeedHash ||
40 opening.clientSeed !== savedReceipt.clientSeed ||
41 Number(opening.nonce) !== 0) return false
42
43 const roll = deriveRoll({ ...opening, nonce: 0, context: opening.boxId })
44 if (roll !== Number(opening.rollUnits)) return false
45 // Walk the canonical selectionTable returned by the verify endpoint, in
46 // its saved order. Never walk animationPlan.items: that is the reel's
47 // display order, shuffled per page load and recorded only for replay.
48 let cumulative = 0
49 for (const entry of selectionTable) {
50 cumulative += Number(entry.chanceUnits)
51 if (roll < cumulative) return entry.itemId === opening.selectedItemId
52 }
53 return false
54}