Son güncelleme:
RaseBox kutu açılışları, ücretsiz kutular, savaşlar ve yükseltmeler için doğrulanabilir adillik modeli kullanır. Her rastgele değer; gizli bir sunucu başlangıç değeri, bir istemci başlangıç değeri, bir nonce ve oyunu tanımlayan bir bağlamdan HMAC-SHA256 ile deterministik olarak üretilir. Oynamadan önce her sunucu başlangıç değerinin özetini (hash) kaydeder ve size bir taahhüt olarak iletiriz: kutu açılışları, ücretsiz kutular ve yükseltmelerde bu özet tarayıcınıza gönderilir ve tarayıcınız onu saklar; savaşlarda ise savaş manifestosunda yayımlanır. Kutu açılışları, ücretsiz kutular ve yükseltmeler her oyunda tek kullanımlık yeni bir sunucu başlangıç değeri ve tarayıcınızın ürettiği yeni rastgelelik kullanır; bu nedenle nonce her zaman 0’dır, çünkü oyunları birbirinden ayırmak için gerekmez. Savaşlarda nonce tur indeksidir ve ilk turda 0’dan başlar. Ön taahhütlerin Eylül 2026’da kullanıma sunulmasından önce oluşturulan kutu, ücretsiz kutu ve yükseltme kayıtları, o dönemde nonce olarak kullanılan hesap oyun sayacını gösterir; eski savaşlar aşağıda açıklanmıştır.
Kutular: kutudaki her eşyanın ağırlıklı bir olasılığı vardır. Üretilen rastgele değer, kazanılan eşyayı seçmek için bu ağırlıklı tabloyla eşleştirilir. Animasyonda eşyaların görünme sırası, sayfa her yüklendiğinde sunucunun bağımsız rastgeleliğiyle karıştırılır, o sayfadaki sonraki çevirmelerde korunur ve tekrar için her açılışla birlikte kaydedilir. Hiçbir başlangıç değerinden türetilmez; rastgele değeri ve doğrulama için kayıtlı sırasını koruyan ağırlıklı tabloyu etkilemez.
Savaşlar: savaş oluşturulduğunda her tur ve yuva için ayrı bir sunucu başlangıç değeri, büyük ödül modlarında ise ek olarak büyük ödül için bir değer üretilir; bunların özetleri savaş manifestosunda yayımlanır. Her oyuncu tarayıcısında üretilen yeni rastgeleliği önceden taahhüt eder, katılımcı listesi dondurulur ve ardından bu katkılar açıklanarak tüm yuvalar için ortak tek bir savaş istemci başlangıç değerinde birleştirilir; botlar kendi rastgeleliklerini eklemez. Tüm açılışlar oynatılmadan önce üretilir ve kaydedilir. Her rastgele değer savaş kimliği, tur, yuva ve kutuyla ilişkilidir ve nonce değeri tur indeksidir (ilk turda 0). Rastgele değer, normal kutu açılışıyla aynı ağırlıklı tablodan eşya seçer. Animasyon yalnızca kayıtlı sonucu gösterir; sonucu belirlemez. Savaş başladığında botlar dahil her katılımcı, her tur için ayrıca bağımsız ve rastgele bir animasyon sırası alır. Bu sıra savaşın animasyon planına kaydedilir, planın kendi özetiyle mühürlenir ve hiçbir rastgele değeri etkilemeden canlı oynatma ve tekrarlar için kullanılır. Ön taahhütlerden önce oluşturulan savaşlarda her oyuncunun kendi istemci başlangıç değeri ve nonce olarak hesap oyun sayacı artı tur indeksi kullanılıyordu; botlar için ise ayrı oluşturulan bir başlangıç değerinin özeti ve nonce olarak yalnızca tur indeksi kullanılıyordu.
Savaş kazananları doğrulanabilir eşya değerlerinden hesaplanır: Normal ve Çılgın sırasıyla en yüksek ve en düşük toplamı karşılaştırır; Son tur ve Son tur en düşük yalnızca son turdaki en yüksek ve en düşük değeri, takımlarda birleştirerek karşılaştırır; Grup sonucu eşit paylaşır. Büyük ödül ve Çılgın büyük ödül, önceden taahhüt edilmiş kendi sunucu başlangıç değerini, savaşın aynı istemci başlangıç değerini ve 0 değerindeki nonce’u kullanan ayrı bir HMAC çekilişi ekler. Ağırlık, Büyük ödül’de oyuncu veya takım toplamıyla doğru; Çılgın büyük ödül’de ters orantılıdır.
Yükseltmeler: olasılık katkınız ve hedef değer üzerinden hesaplanır. Üretilen rastgele değer bu eşikten düşükse yükseltme kazanır; aksi durumda kaybeder.
Savaş sırasında sunucu başlangıç değerlerinin özetleri görünür, değerlerin kendisi gizli kalır. Tamamlandıktan sonra /api/battles/{battleId}/verify uç noktası; her açılışı, toplamı, kazananı, jackpot çekilişini ve ödemeyi bağımsız hesaplamak için gereken başlangıç değerlerini, nonce değerlerini, bağlamları, rastgele değerleri ve sabitlenmiş eşya tablolarını açıklar.
Ücretli veya ücretsiz her açılış kendi sunucu seed değerini kullanır. Tarayıcı yeni istemci rastgeleliği üretmeden önce hash değerini kaydeder. Doğrulama, açıklanan sonucu bu önceki kayıtla karşılaştırır; kaydı olmayan eski açılışlar önceden verilmiş bir taahhüdü kanıtlayamaz.
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}