Última actualización:
RaseBox utiliza un modelo de equidad verificable para las cajas, las cajas gratuitas, las batallas y las mejoras. Cada tirada se genera de forma determinista mediante HMAC-SHA256 a partir de una semilla secreta del servidor, una semilla del cliente, un nonce y un contexto que identifica la jugada. Antes de jugar, guardamos la huella criptográfica de cada semilla del servidor y te la entregamos como compromiso: en las cajas, las cajas gratuitas y las mejoras se envía a tu navegador, que la guarda, y en las batallas se publica en el manifiesto de la batalla. Las cajas, las cajas gratuitas y las mejoras usan en cada jugada una semilla del servidor nueva y de un solo uso, junto con aleatoriedad nueva de tu navegador, por lo que el nonce es siempre 0: no hace falta para distinguir unas jugadas de otras. En las batallas, el nonce es el índice de la ronda y empieza en 0 en la primera ronda. Los registros de cajas, cajas gratuitas y mejoras anteriores a la introducción de los compromisos previos, en septiembre de 2026, muestran el contador de jugadas de la cuenta que entonces se usaba como nonce; las batallas antiguas se describen más abajo.
Cajas: cada artículo tiene una probabilidad ponderada. La tirada generada se compara con esa tabla de probabilidades para seleccionar el artículo ganador. El orden en que aparecen los artículos en la animación se mezcla con aleatoriedad independiente del servidor cada vez que se carga la página, se mantiene en los giros siguientes de esa página y se guarda con cada apertura para poder repetirla. No se deriva de ninguna semilla y no afecta a la tirada ni a la tabla ponderada, que conserva su orden guardado para la verificación.
Batallas: al crear la batalla se genera una semilla del servidor distinta para cada ronda y posición, y otra para el bote en los modos con bote; sus huellas se publican en el manifiesto de la batalla. Cada jugador aporta aleatoriedad nueva de su navegador mediante un compromiso previo, la lista de participantes se congela y después esas aportaciones se revelan y se combinan en una única semilla del cliente de la batalla, común a todas las posiciones; los bots no aportan aleatoriedad propia. Todas las aperturas se generan y guardan antes de reproducirse. Cada tirada queda vinculada al identificador de la batalla, ronda, posición y caja, y su nonce es el índice de la ronda (0 en la primera). La tirada selecciona un artículo de la misma tabla ponderada de una apertura normal. La animación solo muestra el resultado guardado; no lo decide. Al empezar la batalla, cada participante, bots incluidos, recibe además un orden aleatorio e independiente de la animación para cada ronda. Se guarda en el plan de animación de la batalla, protegido por la huella propia de ese plan, y se usa en directo y en las repeticiones sin afectar a ninguna tirada. Las batallas anteriores a los compromisos previos usaban la semilla del cliente de cada jugador y, como nonce, su contador de jugadas más el índice de la ronda; los bots usaban la huella de una semilla generada por separado y solo el índice de la ronda como nonce.
Los ganadores se calculan con esos valores verificables: Normal y Loco comparan, respectivamente, el total más alto y más bajo; Final y Último mínimo solo comparan el valor más alto y más bajo de la última ronda, combinado por equipo; Grupo reparte el resultado por igual. Bote y Bote loco añaden un sorteo HMAC independiente con compromiso previo, que usa su propia semilla del servidor, la misma semilla del cliente de la batalla y un nonce 0. En Bote, el peso es proporcional al total de cada jugador o equipo; en Bote loco, es inversamente proporcional.
Mejoras: la probabilidad se calcula según tu aportación y el valor del objetivo. Si la tirada generada está por debajo de ese umbral, la mejora gana; de lo contrario, pierde.
Durante la batalla se muestran las huellas de las semillas del servidor, mientras que las semillas permanecen ocultas. Al finalizar, el punto de acceso /api/battles/{battleId}/verify revela semillas, nonces, contextos, tiradas y tablas de artículos fijadas para recalcular de forma independiente cada apertura, total, ganador, sorteo y pago.
Cada apertura, de pago o gratuita, usa su propia semilla del servidor. El navegador guarda su hash antes de generar aleatoriedad nueva. La verificación compara el resultado revelado con ese recibo previo; las aperturas antiguas sin recibo no permiten demostrar un compromiso anterior.
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}