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
Decisões e trade-offs
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.
Cada caminho de escrita precisa definir o que fazer no conflito: repetir, aceitar o estado atual ou falhar.
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.
Duas ou três inserções a mais em cada movimento de dinheiro.
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.
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.
Quem tem acesso total ao banco ainda pode reescrever uma cadeia inteira e o seu head; só um digest guardado fora do banco revela isso.
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.
O operador conhece os resultados futuros; eliminar isso exige uma client seed, que não foi implementada.
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.
O 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
