Sobre mimMeus ProjetosAgente DicoMinhas MúsicasLeituras
Real-Time Crash Game – Estudo de caso

Real-Time Crash Game

Um crash game multiplayer em tempo real em que cada centavo movido por uma aposta vai para um ledger de partidas dobradas e cada crash point pode ser verificado pelo jogador.

  • NestJS
  • RabbitMQ
  • PostgreSQL
  • Redis
  • WebSockets

Desafio

Pagar cada jogador exatamente uma vez e conseguir provar isso. Rodadas e saldos ficam em serviços e bancos separados, o RabbitMQ pode reentregar qualquer mensagem, e o jogador precisa confiar que o crash point não foi escolhido depois das apostas.

Abordagem

Games e Wallets têm cada um seu banco PostgreSQL e se falam só por eventos: a aposta nasce PENDING e entra no round depois que o Wallets a debita. Toda mudança de saldo é uma transação balanceada de partidas dobradas contra a conta da casa, com chave de idempotência para que um evento reentregue nunca pague duas vezes, e entra numa cadeia SHA-256 por carteira que o jogador confere no extrato. Cada round publica SHA-256(seed) antes das apostas e revela a seed depois do crash. Optimistic locking em rounds, apostas e carteiras substitui locks distribuídos, e um débito que chega depois do cancelamento da aposta é estornado por um evento de compensação.

Resultado

Um job de reconciliação confere a cada 5 minutos que o ledger soma zero, que cada transação fecha e que a cadeia de cada carteira está íntegra, e o Grafana mostra o resultado ao lado do saldo da casa. Onze ADRs registram cada decisão de consistência com seus trade-offs.

Arquitetura

  1. WebSockets
  2. Saga coreografada

Propriedades do design

Toque em uma parte do diagrama para ver a fonte.

Decisões e trade-offs

  1. Optimistic locking em rounds, apostas e carteiras no lugar de locks

    A mesma aposta pode ser alterada ao mesmo tempo pela confirmação, pelo timeout, pelo cash-out manual e automático e pelo crash. Locks pessimistas ou distribuídos aumentariam a latência e criariam pontos de contenção.

    CustoCada caminho de escrita precisa definir o que fazer no conflito: repetir, aceitar o estado atual ou falhar.

  2. Só o saldo do jogador é guardado; o saldo da casa é a soma dos lançamentos

    Um saldo da casa guardado seria uma linha disputada por todas as apostas de todos os rounds.

    CustoDuas ou três inserções a mais em cada movimento de dinheiro.

  3. Chaves de idempotência no ledger, atrás do inbox de mensagens

    O inbox apaga as linhas processadas depois de 30 dias, então sozinho ele não impediria que uma reentrega tardia pagasse duas vezes.

  4. Uma cadeia de hashes SHA-256 por carteira

    Os triggers append-only impedem a aplicação de editar o ledger, mas não quem tem acesso direto ao banco. A cadeia torna detectável qualquer alteração em uma transação já gravada.

    CustoQuem tem acesso total ao banco ainda pode reescrever uma cadeia inteira e o seu head; só um digest guardado fora do banco revela isso.

  5. Crash points provably fair a partir de uma cadeia de seeds gerada antes

    O jogador precisa da garantia de que o servidor não escolhe o crash point depois de ver as apostas, e consegue verificar qualquer round sem confiar nele.

    CustoO operador conhece os resultados futuros; eliminar isso exige uma client seed, que não foi implementada.

  6. Alvos de auto cash-out em um sorted set do Redis, pagos pelo BullMQ

    A cada tick de 100 ms o servidor acha os alvos atingidos sem varrer as apostas do round, e o atraso da fila nunca muda o valor pago, porque o pagamento usa o multiplicador alvo.

    CustoO Redis vira dependência do Games: se ele cair, o auto cash-out para, mas o cash-out manual continua.

Evidência principal

Ledger de partidas dobradas à prova de adulteração e crash points provably fair

ConcluídoInternet