Dernière mise à jour:
RaseBox utilise un modèle d’équité vérifiable pour les boîtes, les boîtes gratuites, les batailles et les améliorations. Chaque tirage est produit de façon déterministe avec HMAC-SHA256 à partir d’une graine secrète du serveur, d’une graine du client, d’un nonce et d’un contexte qui identifie la partie. Avant que vous ne jouiez, nous enregistrons l’empreinte de chaque graine du serveur et vous la communiquons comme engagement : pour les boîtes, les boîtes gratuites et les améliorations, elle est envoyée à votre navigateur, qui la conserve ; pour les batailles, elle est publiée dans le manifeste de la bataille. Les boîtes, les boîtes gratuites et les améliorations utilisent à chaque partie une nouvelle graine du serveur à usage unique et un nouvel aléa généré par votre navigateur : le nonce vaut donc toujours 0, car il n’est pas nécessaire pour distinguer les parties. Dans les batailles, le nonce correspond à l’indice de la manche et commence à 0 à la première manche. Les enregistrements de boîtes, de boîtes gratuites et d’améliorations antérieurs à l’introduction des engagements préalables, en septembre 2026, affichent le compteur de parties du compte utilisé à l’époque comme nonce ; les anciennes batailles sont décrites plus bas.
Boîtes : chaque objet possède une probabilité pondérée. Le tirage généré est comparé à cette table pour sélectionner l’objet gagnant. L’ordre d’affichage des objets dans l’animation est mélangé avec un aléa indépendant du serveur à chaque chargement de la page, conservé pour les tirages suivants sur cette page et enregistré avec chaque ouverture pour la rediffusion. Il ne dérive d’aucune graine et n’influence ni le tirage ni la table pondérée, qui conserve son ordre enregistré pour la vérification.
Batailles : à la création d’une bataille, une graine du serveur distincte est générée pour chaque manche et chaque place, plus une pour le jackpot dans les modes jackpot ; leurs empreintes sont publiées dans le manifeste de la bataille. Chaque joueur engage au préalable un nouvel aléa généré par son navigateur, la liste des participants est figée, puis ces contributions sont révélées et combinées en une seule graine du client de la bataille, commune à toutes les places ; les robots n’apportent aucun aléa propre. Toutes les ouvertures sont générées et enregistrées avant leur animation. Chaque tirage est lié à l’identifiant de bataille, à la manche, à la place et à la boîte, et son nonce est l’indice de la manche (0 à la première manche). Le tirage sélectionne un objet dans la même table pondérée qu’une ouverture normale. L’animation présente uniquement le résultat enregistré ; elle ne le décide pas. Au début de la bataille, chaque participant, robots compris, reçoit aussi un ordre d’animation aléatoire et indépendant pour chaque manche. Il est enregistré dans le plan d’animation de la bataille, scellé par l’empreinte propre de ce plan, et sert à l’affichage en direct et aux rediffusions sans influencer aucun tirage. Les batailles antérieures aux engagements préalables utilisaient la graine du client de chaque joueur et, comme nonce, son compteur de parties plus l’indice de la manche ; les robots utilisaient l’empreinte d’une graine générée séparément et uniquement l’indice de la manche comme nonce.
Les gagnants sont calculés à partir de ces valeurs vérifiables : Normal et Fou comparent respectivement le total le plus élevé et le plus faible ; Final et Dernier minimum comparent uniquement la valeur la plus élevée et la plus faible de la dernière manche, cumulée par équipe ; Groupe partage le résultat également. Jackpot et Jackpot fou ajoutent un tirage HMAC distinct avec sa propre graine du serveur engagée au préalable, la même graine du client de la bataille et un nonce égal à 0. Le poids est proportionnel au total du joueur ou de l’équipe pour Jackpot et inversement proportionnel pour Jackpot fou.
Améliorations : la probabilité est calculée à partir de votre contribution et de la valeur cible. Si le tirage est inférieur à ce seuil, l’amélioration réussit ; sinon, elle échoue.
Pendant une bataille, les empreintes des graines du serveur sont visibles et les graines restent cachées. Après la fin, le point d’accès /api/battles/{battleId}/verify révèle les graines, nonces, contextes, tirages et tables d’objets figées permettant de recalculer indépendamment chaque ouverture, total, gagnant, tirage du jackpot et paiement.
Chaque ouverture, payante ou gratuite, utilise sa propre graine serveur. Le navigateur enregistre son hash avant de générer un nouvel aléa client. La vérification compare le résultat révélé à ce reçu préalable ; une ancienne ouverture sans reçu ne prouve pas un engagement antérieur.
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}