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 ProCases é 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
hashServerSeed(seed: string): string {
  // Commitment = HMAC-SHA256 com o seed como chave e mensagem VAZIA (não é sha256(seed) puro)
  return crypto.createHmac('sha256', seed).digest('hex');
}

// Entrada: "a7b9c2d4e6f8..."
// Saída:   "8f3a2b1c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1"

3. Cálculo do Roll

typescript
calculateRoll(
  serverSeed: string,
  clientSeed: string,
  nonce: number,
  max: number = 10000000,
): number {
  const message = `${clientSeed}:${nonce}`;

  const hmac = crypto.createHmac('sha256', serverSeed)
    .update(message)
    .digest('hex');

  const hashPrefix = hmac.substring(0, 13); // 13 chars hex = 52 bits
  const decimal = parseInt(hashPrefix, 16);
  const maxSafeValue = Math.floor(Number.MAX_SAFE_INTEGER / max) * max;

  if (decimal >= maxSafeValue) {
    // Dead zone: re-sorteia anexando ':retry' ao MESMO nonce (não incrementa o nonce)
    const retry = crypto.createHmac('sha256', serverSeed)
      .update(`${clientSeed}:${nonce}:retry`)
      .digest('hex');
    return (parseInt(retry.substring(0, 13), 16) % max) + 1;
  }

  return (decimal % max) + 1; // Range 1-based: 1 a 10.000.000
}

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

Detalhes importantes:

  • substring(0, 13) = 52 bits de entropia (não 8 chars / 32 bits)
  • serverSeed é a chave HMAC, clientSeed:nonce é a mensagem
  • Dead zone: se decimal >= maxSafeValue, re-sorteia anexando o sufixo :retry ao MESMO nonce (elimina viés de módulo — não incrementa o nonce)

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 ProCases

ProvablyFairService

typescript
// src/application/services/provably-fair.service.ts

@Injectable()
export class ProvablyFairService {
  generateServerSeed(): string {
    return crypto.randomBytes(32).toString('hex');
  }

  generateClientSeed(userInput?: string): string {
    // Se o jogador fornece o próprio clientSeed, usa ele; senão gera 32 bytes
    if (userInput && userInput.length > 0) return userInput;
    return crypto.randomBytes(32).toString('hex');
  }

  hashServerSeed(seed: string): string {
    // Commitment = HMAC-SHA256 com o seed como chave e mensagem VAZIA (não é sha256(seed) puro)
    return crypto.createHmac('sha256', seed).digest('hex');
  }

  calculateRoll(
    serverSeed: string,
    clientSeed: string,
    nonce: number,
    max: number = 10000000,
  ): number {
    const message = `${clientSeed}:${nonce}`;
    const hmac = crypto.createHmac('sha256', serverSeed)
      .update(message)
      .digest('hex');

    const hashPrefix = hmac.substring(0, 13);
    const decimal = parseInt(hashPrefix, 16);
    const maxSafeValue = Math.floor(Number.MAX_SAFE_INTEGER / max) * max;

    if (decimal >= maxSafeValue) {
      const retry = crypto.createHmac('sha256', serverSeed)
        .update(`${clientSeed}:${nonce}:retry`)
        .digest('hex');
      return (parseInt(retry.substring(0, 13), 16) % max) + 1;
    }

    return (decimal % max) + 1;
  }

  // Confere que o serverSeed revelado corresponde ao hash publicado ANTES da aposta
  verifyServerSeedHash(serverSeed: string, serverSeedHash: string): boolean {
    return this.hashServerSeed(serverSeed) === serverSeedHash;
  }

  // Recalcula o roll a partir dos seeds gravados e compara com o resultado
  verifyResult(
    serverSeed: string,
    clientSeed: string,
    nonce: number,
    expectedRoll: number,
    max: number = 10000000,
  ): boolean {
    return this.calculateRoll(serverSeed, clientSeed, nonce, max) === expectedRoll;
  }
}

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.hashServerSeed(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');
  }

  // Verificação em 2 passos (os métodos reais do ProvablyFairService)
  const hashValid = this.provablyFairService.verifyServerSeedHash(
    opening.serverSeed,
    opening.serverSeedHash,
  );
  const rollValid = this.provablyFairService.verifyResult(
    opening.serverSeed,
    opening.clientSeed,
    opening.nonce,
    opening.roll,
  );

  return {
    isValid: hashValid && rollValid,
    hashValid,
    rollValid,
    serverSeed: opening.serverSeed,
    serverSeedHash: opening.serverSeedHash,
    clientSeed: opening.clientSeed,
    nonce: opening.nonce,
    roll: opening.roll,
    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 = HMAC-SHA256(key=serverSeed, "") = "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 (HMAC-SHA256 com o serverSeed como chave e mensagem vazia)
const hash = crypto.createHmac('sha256', 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 hashPrefix = hmac.substring(0, 13);
const decimal = parseInt(hashPrefix, 16);
const roll = (decimal % 10000000) + 1;
console.log(roll); // Deve bater com o roll do jogo

Sistema FLIP e Provably Fair

O FLIP usa o mesmo roll da abertura — não há um segundo sorteio nem um segundo conjunto de seeds. Cada caixa tem um flipThreshold (nullable; null = sem FLIP). Quando o roll cai no topo do range, a seleção acontece no tier de itens raros:

typescript
// Roll único da abertura
const roll = calculateRoll(serverSeed, clientSeed, nonce);

// FLIP ativa quando o roll passa do threshold da caixa
const flipThreshold = caseData.flipThreshold; // dinâmico por caixa, nullable
const isFlip = flipThreshold !== null && roll >= flipThreshold;

if (isFlip) {
  // Itens raros = os que começam acima do threshold (hashRangeStart >= flipThreshold)
  const flipItems = getFlipItems(caseItems, flipThreshold);

  // O MESMO roll seleciona o item raro (nenhum roll novo)
  wonItem = selectFlipWinner(flipItems, roll);
}

FLIP é um único roll

Não existe flipRoll independente. Na abertura de caixa, as colunas flipServerSeed/flipClientSeed/flipNonce/flipRoll são gravadas como null — o FLIP hoje é essencialmente um flag visual (isFlip) que dispara a animação de roleta dupla no front, enquanto a seleção do item usa o roll único. A verificação pública recalcula esse mesmo roll.


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 - ProCases