Ultimo aggiornamento:
RaseBox utilizza un modello di equità verificabile per casse, casse gratuite, battaglie e potenziamenti. Ogni estrazione viene generata in modo deterministico con HMAC-SHA256 a partire da un seme segreto del server, un seme del client, un nonce e un contesto che identifica la giocata. Prima che tu giochi, memorizziamo l’impronta crittografica di ogni seme del server e te la forniamo come impegno: per casse, casse gratuite e potenziamenti viene inviata al tuo browser, che la salva; per le battaglie viene pubblicata nel manifesto della battaglia. Casse, casse gratuite e potenziamenti usano a ogni giocata un nuovo seme del server monouso e nuova casualità generata dal tuo browser, quindi il nonce è sempre 0: non serve per distinguere una giocata dall’altra. Nelle battaglie, il nonce è l’indice del turno e parte da 0 nel primo turno. I record di casse, casse gratuite e potenziamenti precedenti all’introduzione degli impegni preventivi, a settembre 2026, mostrano il contatore di giocate dell’account usato allora come nonce; le battaglie precedenti sono descritte più avanti.
Casse: ogni oggetto ha una probabilità ponderata. L’estrazione generata viene confrontata con questa tabella per selezionare l’oggetto vincente. L’ordine in cui gli oggetti compaiono nell’animazione viene mescolato con casualità indipendente del server a ogni caricamento della pagina, mantenuto per i giri successivi su quella pagina e salvato con ogni apertura per la riproduzione. Non deriva da alcun seme e non influisce né sull’estrazione né sulla tabella ponderata, che conserva il suo ordine salvato per la verifica.
Battaglie: alla creazione di una battaglia viene generato un seme del server distinto per ogni turno e posizione, più uno per il jackpot nelle modalità jackpot; le loro impronte vengono pubblicate nel manifesto della battaglia. Ogni giocatore impegna in anticipo nuova casualità generata dal proprio browser, l’elenco dei partecipanti viene congelato e poi questi contributi vengono rivelati e combinati in un unico seme del client della battaglia, comune a tutte le posizioni; i bot non aggiungono casualità propria. Tutte le aperture vengono generate e memorizzate prima della riproduzione. Ogni estrazione è legata all’identificativo della battaglia, al turno, alla posizione e alla cassa, e il suo nonce è l’indice del turno (0 nel primo turno). L’estrazione seleziona un oggetto dalla stessa tabella ponderata di un’apertura normale. L’animazione mostra solo il risultato memorizzato; non lo determina. All’inizio della battaglia ogni partecipante, bot compresi, riceve anche un ordine casuale e indipendente dell’animazione per ogni turno. Viene salvato nel piano di animazione della battaglia, sigillato dall’impronta propria di quel piano, e usato per la riproduzione dal vivo e i replay senza influire su alcuna estrazione. Le battaglie precedenti agli impegni preventivi usavano il seme del client di ciascun giocatore e, come nonce, il suo contatore di giocate più l’indice del turno; i bot usavano l’impronta di un seme generato separatamente e solo l’indice del turno come nonce.
I vincitori vengono calcolati da questi valori verificabili: Normal e Pazzo confrontano rispettivamente il totale più alto e più basso; Finale e Ultimo minimo solo il valore più alto e più basso dell’ultimo turno, combinato per le squadre; Gruppo divide il risultato equamente. Jackpot e Jackpot pazzo aggiungono un’estrazione HMAC separata, con un proprio seme del server impegnato in anticipo, lo stesso seme del client della battaglia e un nonce pari a 0. Il peso è proporzionale al totale di ciascun giocatore o squadra per Jackpot e inversamente proporzionale per Jackpot pazzo.
Potenziamenti: la probabilità è calcolata in base al contributo e al valore dell’obiettivo. Se l’estrazione generata è inferiore alla soglia, il potenziamento riesce; altrimenti fallisce.
Durante una battaglia, le impronte dei semi del server sono visibili mentre i semi restano nascosti. Al termine, il punto di accesso /api/battles/{battleId}/verify rivela semi, nonce, contesti, estrazioni e tabelle degli oggetti congelate, necessarie per ricalcolare autonomamente ogni apertura, totale, vincitore, estrazione del jackpot e pagamento.
Ogni apertura, a pagamento o gratuita, usa un proprio seed del server. Il browser ne salva l’hash prima di generare nuova casualità. La verifica confronta il risultato rivelato con quella ricevuta precedente; le vecchie aperture senza ricevuta non dimostrano un impegno precedente.
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}