Última atualização:
A RaseBox utiliza um modelo de equidade verificável para caixas, caixas gratuitas, batalhas e melhorias. Cada sorteio é gerado de forma determinística com HMAC-SHA256 a partir de uma semente secreta do servidor, uma semente do cliente, um nonce e um contexto que identifica a jogada. Antes de jogar, guardamos a impressão criptográfica de cada semente do servidor e entregamo-la como compromisso: nas caixas, caixas gratuitas e melhorias é enviada para o seu navegador, que a guarda; nas batalhas é publicada no manifesto da batalha. As caixas, as caixas gratuitas e as melhorias usam em cada jogada uma nova semente do servidor de utilização única e nova aleatoriedade gerada pelo seu navegador, pelo que o nonce é sempre 0: não é necessário para distinguir as jogadas. Nas batalhas, o nonce é o índice da ronda e começa em 0 na primeira ronda. Os registos de caixas, caixas gratuitas e melhorias anteriores à introdução dos compromissos prévios, em setembro de 2026, mostram o contador de jogadas da conta que era então usado como nonce; as batalhas antigas são descritas mais abaixo.
Caixas: cada artigo tem uma probabilidade ponderada. O sorteio gerado é comparado com essa tabela para selecionar o artigo vencedor. A ordem em que os artigos aparecem na animação é baralhada com aleatoriedade independente do servidor sempre que a página é carregada, mantida nos giros seguintes dessa página e guardada com cada abertura para a repetição. Não deriva de nenhuma semente e não afeta o sorteio nem a tabela ponderada, que mantém a sua ordem guardada para a verificação.
Batalhas: quando uma batalha é criada, é gerada uma semente do servidor distinta para cada ronda e posição, mais uma para o jackpot nos modos jackpot; as respetivas impressões são publicadas no manifesto da batalha. Cada jogador apresenta previamente um compromisso sobre nova aleatoriedade gerada pelo seu navegador, a lista de participantes é congelada e, depois, essas contribuições são reveladas e combinadas numa única semente do cliente da batalha, comum a todas as posições; os robôs não acrescentam aleatoriedade própria. Todas as aberturas são geradas e guardadas antes da reprodução. Cada sorteio fica associado ao identificador da batalha, ronda, posição e caixa, e o seu nonce é o índice da ronda (0 na primeira ronda). O sorteio seleciona um artigo da mesma tabela ponderada de uma abertura normal. A animação apenas apresenta o resultado guardado; não o decide. No início da batalha, cada participante, robôs incluídos, recebe também uma ordem aleatória e independente da animação para cada ronda. Fica guardada no plano de animação da batalha, selada pela impressão própria desse plano, e é usada na reprodução em direto e nas repetições sem afetar nenhum sorteio. As batalhas anteriores aos compromissos prévios usavam a semente do cliente de cada jogador e, como nonce, o seu contador de jogadas mais o índice da ronda; os robôs usavam a impressão de uma semente gerada separadamente e apenas o índice da ronda como nonce.
Os vencedores são calculados a partir desses valores verificáveis: Normal e Louco comparam o maior e o menor total, respetivamente; Final e Último mínimo comparam apenas o maior e o menor valor da última ronda, combinado por equipa; Grupo divide o resultado igualmente. Jackpot e Jackpot louco acrescentam um sorteio HMAC separado, sujeito a compromisso prévio, com a sua própria semente do servidor, a mesma semente do cliente da batalha e um nonce igual a 0. O peso é proporcional ao total de cada jogador ou equipa em Jackpot e inversamente proporcional em Jackpot louco.
Melhorias: a probabilidade é calculada a partir da sua contribuição e do valor do objetivo. Se o sorteio gerado ficar abaixo desse limiar, a melhoria ganha; caso contrário, perde.
Durante uma batalha, as impressões das sementes do servidor são visíveis, enquanto as sementes ficam ocultas. Após a conclusão, o ponto de acesso /api/battles/{battleId}/verify revela sementes, nonces, contextos, sorteios e tabelas de artigos fixadas para recalcular independentemente cada abertura, total, vencedor, sorteio do jackpot e pagamento.
Cada abertura, paga ou gratuita, usa a sua própria seed do servidor. O navegador guarda o hash antes de gerar nova aleatoriedade. A verificação compara o resultado revelado com esse recibo prévio; aberturas antigas sem recibo não comprovam um compromisso 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}