Provably Fair
O sistema Provably Fair é fundamental em gambling online. Permite que jogadores verifiquem matematicamente que os resultados não foram manipulados.
O que é Provably Fair?
É um método criptográfico que garante:
- O site não pode manipular o resultado após a aposta
- O jogador pode verificar que o resultado foi justo
- Transparência total sobre como resultados são gerados
Como Funciona?
Algoritmo
1. Geração do Server Seed
typescript
generateServerSeed(): string {
// 32 bytes aleatórios = 64 caracteres hex
return crypto.randomBytes(32).toString('hex');
}
// Exemplo: "a7b9c2d4e6f8g0h1i3j5k7l9m1n3o5p7q9r1s3t5u7v9w1x3y5z7"2. Hash do Server Seed
typescript
hashSeed(seed: string): string {
return crypto.createHash('sha256').update(seed).digest('hex');
}
// Entrada: "a7b9c2d4e6f8..."
// Saída: "8f3a2b1c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1"3. Cálculo do Roll
typescript
calculateRoll(serverSeed: string, clientSeed: string, nonce: number): number {
// Combina seeds com nonce
const message = `${clientSeed}:${nonce}`;
// HMAC-SHA256
const hmac = crypto.createHmac('sha256', serverSeed)
.update(message)
.digest('hex');
// Pega os primeiros 8 caracteres (32 bits)
const hex = hmac.substring(0, 8);
// Converte para número (0 a 4.294.967.295)
const decimal = parseInt(hex, 16);
// Normaliza para 0 a 99.999 (100.000 possibilidades)
return decimal % 100000;
}
// Exemplo:
// serverSeed: "a7b9c2d4..."
// clientSeed: "player123"
// nonce: 1
// Resultado: 45382 (significa 45.382%)4. Seleção do Item
Os itens têm faixas de roll (hash ranges):
typescript
// Exemplo de caixa com 4 itens:
// AWP Dragon Lore: 1 - 10000 (0.1% chance)
// AK-47 Fire Serpent: 10001 - 60000 (0.5% chance)
// M4A4 Howl: 60001 - 460000 (4% chance)
// Glock Fade: 460001 - 10000000 (95.4% chance)
selectItem(items: CaseItem[], roll: number): Item {
for (const item of items) {
if (roll >= item.hashRangeStart && roll <= item.hashRangeEnd) {
return item;
}
}
throw new Error('No item found for roll');
}
// Se roll = 4538200:
// 4538200 >= 460001 && 4538200 <= 10000000 ✓
// Resultado: Glock FadeImplementação no CSGOFlip
ProvablyFairService
typescript
// src/application/services/provably-fair.service.ts
@Injectable()
export class ProvablyFairService {
generateServerSeed(): string {
return crypto.randomBytes(32).toString('hex');
}
generateClientSeed(): string {
return crypto.randomBytes(16).toString('hex');
}
hashSeed(seed: string): string {
return crypto.createHash('sha256').update(seed).digest('hex');
}
calculateRoll(
serverSeed: string,
clientSeed: string,
nonce: number,
): number {
const message = `${clientSeed}:${nonce}`;
const hmac = crypto.createHmac('sha256', serverSeed)
.update(message)
.digest('hex');
return parseInt(hmac.substring(0, 8), 16) % 100000;
}
verify(
serverSeed: string,
serverSeedHash: string,
clientSeed: string,
nonce: number,
expectedRoll: number,
): VerificationResult {
// 1. Verifica que o hash bate
const calculatedHash = this.hashSeed(serverSeed);
if (calculatedHash !== serverSeedHash) {
return {
isValid: false,
error: 'Server seed hash does not match',
};
}
// 2. Recalcula o roll
const calculatedRoll = this.calculateRoll(serverSeed, clientSeed, nonce);
if (calculatedRoll !== expectedRoll) {
return {
isValid: false,
error: 'Roll does not match',
expectedRoll,
calculatedRoll,
};
}
return {
isValid: true,
serverSeed,
serverSeedHash,
clientSeed,
nonce,
roll: calculatedRoll,
};
}
}Uso em Case Opening
typescript
// src/application/use-cases/case-opening/open-case.use-case.ts
async execute(userId: bigint, caseId: bigint, clientSeed?: string) {
// 1. Gera seeds
const serverSeed = this.provablyFairService.generateServerSeed();
const serverSeedHash = this.provablyFairService.hashSeed(serverSeed);
const finalClientSeed = clientSeed || this.provablyFairService.generateClientSeed();
// 2. Busca próximo nonce do usuário
const nonce = await this.openingRepository.getNextNonce(userId);
// 3. Calcula roll
const roll = this.provablyFairService.calculateRoll(
serverSeed,
finalClientSeed,
nonce,
);
// 4. Seleciona item baseado no roll
const items = await this.caseRepository.getItemsWithRanges(caseId);
const wonItem = this.selectItem(items, roll);
// 5. Salva todos os dados para verificação futura
const opening = await this.openingRepository.create({
userId,
caseId,
itemId: wonItem.id,
serverSeed, // Revelado após o jogo
serverSeedHash, // Compromisso (enviado antes)
clientSeed: finalClientSeed,
nonce,
roll,
});
return {
id: opening.id,
item: wonItem,
serverSeedHash, // Jogador recebe isso ANTES de ver o resultado
};
}Verificação pelo Usuário
typescript
// src/application/use-cases/case-opening/verify-opening.use-case.ts
async execute(userId: bigint, openingId: bigint) {
const opening = await this.openingRepository.findById(openingId);
if (opening.userId !== userId) {
throw new ForbiddenException('Not your opening');
}
const verification = this.provablyFairService.verify(
opening.serverSeed,
opening.serverSeedHash,
opening.clientSeed,
opening.nonce,
opening.roll,
);
return {
...verification,
item: opening.item,
itemRangeStart: opening.item.hashRangeStart,
itemRangeEnd: opening.item.hashRangeEnd,
};
}Por que é Seguro?
1. Server Seed é Secreto Até o Fim
ANTES do jogo:
- Servidor tem: serverSeed = "abc123..."
- Jogador tem: serverSeedHash = SHA256("abc123...") = "8f3a2b..."
O jogador NÃO SABE o serverSeed, então não pode prever o resultado.
O servidor NÃO PODE MUDAR o serverSeed, porque mudaria o hash.2. Client Seed Adiciona Imprevisibilidade
- O jogador pode fornecer seu próprio clientSeed
- Mesmo que o servidor soubesse o clientSeed com antecedência,
não poderia escolher um serverSeed que desse um resultado específico
(teria que adivinhar o hash inverso - impossível)3. Nonce Garante Unicidade
- Cada jogo tem um nonce diferente (incrementado)
- Mesmo com os mesmos seeds, cada jogo tem resultado diferente
- Impossível reutilizar um resultado favorável4. Verificação Independente
javascript
// O jogador pode verificar em qualquer calculadora online ou localmente:
const crypto = require('crypto');
// Dados do jogo
const serverSeed = "a7b9c2d4e6f8..."; // Revelado após o jogo
const clientSeed = "player123";
const nonce = 5;
// Verifica o hash
const hash = crypto.createHash('sha256').update(serverSeed).digest('hex');
console.log(hash); // Deve bater com serverSeedHash recebido ANTES
// Recalcula o roll
const hmac = crypto.createHmac('sha256', serverSeed)
.update(`${clientSeed}:${nonce}`)
.digest('hex');
const roll = parseInt(hmac.substring(0, 8), 16) % 100000;
console.log(roll); // Deve bater com o roll do jogoSistema FLIP e Provably Fair
O sistema FLIP usa dois conjuntos de seeds:
typescript
// Roll normal (determina se FLIP ativa)
const roll = calculateRoll(serverSeed, clientSeed, nonce);
const flipThreshold = await getFlipThreshold(caseId); // Dinâmico por caixa
const isFlip = flipThreshold !== null && roll >= flipThreshold;
if (isFlip) {
// Roll FLIP (determina qual item raro ganha)
const flipServerSeed = serverSeed + '_flip';
const flipNonce = nonce + 1;
const flipRoll = calculateRoll(flipServerSeed, clientSeed, flipNonce);
// Seleciona item da roleta de raros
const rareItem = selectFromRareItems(flipRoll);
}
// Ambos os conjuntos são salvos para verificação:
// - serverSeed, clientSeed, nonce, roll (normal)
// - flipServerSeed, clientSeed, flipNonce, flipRoll (FLIP)Interface do Usuário
Antes do Jogo
json
// Resposta da API ao abrir caixa
{
"id": "123456",
"serverSeedHash": "8f3a2b1c9d8e7f6a...", // Compromisso
"item": {
"name": "AK-47 | Redline",
"value": 2500
}
}Verificação
json
// Resposta da API ao verificar
{
"isValid": true,
"serverSeed": "a7b9c2d4e6f8...", // Agora revelado
"serverSeedHash": "8f3a2b1c9d8e7f6a...",
"clientSeed": "player123",
"nonce": 5,
"roll": 45382,
"item": {
"name": "AK-47 | Redline",
"hashRangeStart": 40000,
"hashRangeEnd": 60000 // 45382 está nessa faixa ✓
}
}Cálculo de Probabilidades
Configuração dos Ranges
typescript
// Ao criar/editar uma caixa, calculamos os ranges
function calculateHashRanges(items: { item: Item; weight: number }[]) {
// Total de peso
const totalWeight = items.reduce((sum, i) => sum + i.weight, 0);
let currentPosition = 0;
return items.map(item => {
// Calcula quantos "slots" este item ocupa
const slots = Math.round((item.weight / totalWeight) * 100000);
const range = {
itemId: item.item.id,
hashRangeStart: currentPosition,
hashRangeEnd: currentPosition + slots - 1,
dropChance: (slots / 100000) * 100, // Porcentagem
};
currentPosition += slots;
return range;
});
}
// Exemplo:
// Item A: weight 1 → range 1-100000 (1%)
// Item B: weight 9 → range 100001-1000000 (9%)
// Item C: weight 90 → range 1000001-10000000 (90%)Segurança Adicional
Rotação de Seeds
Cada usuário tem um nonce que incrementa:
typescript
async getNextNonce(userId: bigint): Promise<number> {
const result = await this.prisma.caseOpening.aggregate({
where: { userId },
_max: { nonce: true },
});
return (result._max.nonce || 0) + 1;
}Auditoria
Todas as aberturas são logadas com dados completos:
typescript
// Audit log automático
{
action: 'CASE_OPENING',
userId: 123,
entityId: 456,
metadata: {
caseId: 789,
serverSeedHash: '8f3a2b...',
roll: 45382,
itemId: 101,
itemValue: 2500,
}
}Arquivos Fonte Relacionados
Principais Arquivos
src/application/services/provably-fair.service.ts- Algoritmo principalsrc/application/use-cases/case-opening/open-case.use-case.ts- Uso em aberturassrc/application/use-cases/case-opening/verify-opening.use-case.ts- Verificaçãosrc/presentation/controllers/provably-fair.controller.ts- Endpoints de verificação
