Skip to content

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:

  1. O site não pode manipular o resultado após a aposta
  2. O jogador pode verificar que o resultado foi justo
  3. 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 Fade

Implementaçã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ável

4. 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 jogo

Sistema 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 principal
  • src/application/use-cases/case-opening/open-case.use-case.ts - Uso em aberturas
  • src/application/use-cases/case-opening/verify-opening.use-case.ts - Verificação
  • src/presentation/controllers/provably-fair.controller.ts - Endpoints de verificação

Documentação Técnica CSGOFlip