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.

RANGE OFICIAL

O range do sistema Provably Fair do CSGOFlip é de 1 a 10.000.000 (dez milhões).

Todos os cálculos de roll, hashRange e successThreshold DEVEM usar este range.

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 1 a 10.000.000 (dez milhões de possibilidades)
  return (decimal % 10000000) + 1;
}

// Exemplo:
// serverSeed: "a7b9c2d4..."
// clientSeed: "player123"
// nonce: 1
// Resultado: 4538200 (roll de 1 a 10.000.000)

4. Seleção do Item

Os itens têm faixas de roll (hash ranges) de 1 a 10.000.000:

typescript
// Exemplo de caixa com 4 itens (range total: 1 a 10.000.000):
// 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');
    
    // Range: 1 a 10.000.000
    return (parseInt(hmac.substring(0, 8), 16) % 10000000) + 1;
  }

  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

O range total é de 1 a 10.000.000 (dez milhões de possibilidades).

typescript
const HASH_MAX = 10000000;

// 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 = 1; // Começa em 1
  
  return items.map((item, index, arr) => {
    // Calcula quantos "slots" este item ocupa
    const slots = Math.floor((item.weight / totalWeight) * HASH_MAX);
    
    // Último item vai até HASH_MAX para cobrir arredondamentos
    const rangeEnd = index === arr.length - 1 
      ? HASH_MAX 
      : currentPosition + slots - 1;
    
    const range = {
      itemId: item.item.id,
      hashRangeStart: currentPosition,
      hashRangeEnd: rangeEnd,
      dropChance: (slots / HASH_MAX) * 100, // Porcentagem
    };
    
    currentPosition = rangeEnd + 1;
    return range;
  });
}

// Exemplo com HASH_MAX = 10.000.000:
// 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