Zuletzt aktualisiert:
RaseBox verwendet ein Modell nachprüfbarer Fairness für Kistenöffnungen, Gratiskisten, Kämpfe und Verbesserungen. Jeder Zufallswert wird mit HMAC-SHA256 deterministisch aus einem geheimen Server-Startwert, einem Client-Startwert, einer Nonce und einem Kontext erzeugt, der das Spiel identifiziert. Vor dem Spiel speichern wir den Hash jedes Server-Startwerts und geben ihn dir als Festlegung: Bei Kistenöffnungen, Gratiskisten und Verbesserungen wird er an deinen Browser gesendet, der ihn speichert; bei Kämpfen wird er im Manifest des Kampfes veröffentlicht. Kistenöffnungen, Gratiskisten und Verbesserungen verwenden bei jedem Spiel einen neuen, nur einmal nutzbaren Server-Startwert und neue Zufallsdaten deines Browsers. Deshalb ist die Nonce immer 0: Sie wird nicht benötigt, um Spiele voneinander zu unterscheiden. In Kämpfen entspricht die Nonce dem Rundenindex und beginnt in der ersten Runde bei 0. Einträge zu Kistenöffnungen, Gratiskisten und Verbesserungen aus der Zeit vor Einführung der vorherigen Festlegungen im September 2026 zeigen den damals als Nonce verwendeten Spielzähler des Kontos; ältere Kämpfe werden weiter unten beschrieben.
Kisten: Jeder Gegenstand hat eine gewichtete Wahrscheinlichkeit. Der erzeugte Zufallswert wird dieser Tabelle zugeordnet, um den gewonnenen Gegenstand auszuwählen. Die Reihenfolge der Gegenstände in der Animation wird bei jedem Laden der Seite mit unabhängigen Zufallsdaten des Servers gemischt, für die folgenden Drehungen auf dieser Seite beibehalten und mit jeder Öffnung für die Wiederholung gespeichert. Sie wird aus keinem Startwert abgeleitet und beeinflusst weder den Zufallswert noch die gewichtete Tabelle, die für die Prüfung ihre gespeicherte Reihenfolge behält.
Kämpfe: Beim Erstellen eines Kampfes wird für jede Runde und jeden Platz ein eigener Server-Startwert erzeugt, in Jackpot-Modi zusätzlich einer für den Jackpot; ihre Hashes werden im Manifest des Kampfes veröffentlicht. Jeder menschliche Spieler legt neue Zufallsdaten aus seinem Browser vorab fest, die Teilnehmerliste wird eingefroren, und anschließend werden diese Beiträge offengelegt und zu einem gemeinsamen Client-Startwert des Kampfes für alle Plätze kombiniert; Bots steuern keine eigenen Zufallsdaten bei. Alle Öffnungen werden vor der Wiedergabe erzeugt und gespeichert. Jeder Zufallswert ist an Kampfkennung, Runde, Platz und Kiste gebunden, und seine Nonce ist der Rundenindex (0 in der ersten Runde). Der Zufallswert wählt aus derselben gewichteten Tabelle wie bei einer normalen Kistenöffnung. Die Animation zeigt nur das gespeicherte Ergebnis und bestimmt es nicht. Zu Kampfbeginn erhält außerdem jeder Teilnehmer, Bots eingeschlossen, für jede Runde eine unabhängige zufällige Reihenfolge der Animation. Sie wird im Animationsplan des Kampfes gespeichert, durch dessen eigenen Hash gesichert und für die Live-Wiedergabe und Wiederholungen verwendet, ohne einen Zufallswert zu beeinflussen. Kämpfe aus der Zeit vor den vorherigen Festlegungen verwendeten den Client-Startwert jedes Spielers und als Nonce seinen Spielzähler plus Rundenindex; Bots verwendeten den Hash eines gesondert erzeugten Startwerts und nur den Rundenindex als Nonce.
Die Gewinner werden aus diesen überprüfbaren Gegenstandswerten berechnet: Normal und Verrückt vergleichen die höchste beziehungsweise niedrigste Summe; Finale und Letztes Minimum nur den höchsten beziehungsweise niedrigsten Wert der letzten Runde, bei Teams zusammengefasst; Gruppe teilt das Ergebnis gleichmäßig. Jackpot und Verrückter Jackpot ergänzen eine gesonderte HMAC-Ziehung mit einem eigenen, vorher festgelegten Server-Startwert, demselben Client-Startwert des Kampfes und der Nonce 0. Bei Jackpot ist das Gewicht proportional zur Summe jedes Spielers oder Teams, bei Verrückter Jackpot umgekehrt proportional.
Verbesserungen: Die Wahrscheinlichkeit wird aus deinem Beitrag und dem Zielwert berechnet. Liegt der erzeugte Zufallswert unter dieser Schwelle, ist die Verbesserung erfolgreich; andernfalls scheitert sie.
Während eines Kampfes sind die Hashes der Server-Startwerte sichtbar, die Startwerte selbst bleiben verborgen. Nach Abschluss veröffentlicht der Endpunkt /api/battles/{battleId}/verify Startwerte, Nonces, Kontexte, Zufallswerte und eingefrorene Gegenstandstabellen, mit denen sich jede Öffnung, Summe, Gewinnerermittlung, Jackpot-Ziehung und Auszahlung unabhängig nachrechnen lässt.
Jede kostenpflichtige oder kostenlose Öffnung verwendet einen eigenen Server-Seed. Der Browser speichert dessen Hash, bevor er neue Client-Zufallsdaten erzeugt. Die Prüfung vergleicht das offengelegte Ergebnis mit diesem früheren Beleg; alte Öffnungen ohne Beleg beweisen keine vorherige Festlegung.
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}