Files
casino/pkg/fair/fair.go
drjones dee3becd47 fix(sim): cap crash point so extreme seeds cannot overflow or bankrupt
At u=1 the unsigned quotient exceeded int64 and wrapped negative, so the
rarest and most valuable outcome silently became an instant 1.00x loss.
At u=2 it produced a 2.1-billion-times payout the house could never
cover, which would have left settlement failing and the player unpaid.
The crash point is now capped at the largest multiplier the curve can
express, which is unreachable anyway since the round hits its tick
ceiling first.

FromInt now panics outside the Q32.32 integer range instead of wrapping
a positive input into a negative value.

Raises coverage to 88% overall; adds a Makefile with db-reset, since the
append-only ledger steadily consumes bridge headroom across test runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:52:50 +00:00

128 lines
4.6 KiB
Go

// Package fair implements the commit-reveal protocol that makes every outcome
// independently verifiable.
//
// The protocol, per round:
//
// 1. Commit — the server generates a random 32-byte seed and publishes
// SHA-256(seed) before betting opens. It is now bound to that seed.
// 2. Client seed — derived from the public keys of everyone in the round.
// The operator does not control these, so it cannot steer the outcome even
// with full knowledge of its own seed.
// 3. Outcome — HMAC-SHA256(serverSeed, clientSeed || nonce) seeds the
// simulation. The result is a pure function of that seed.
// 4. Reveal — after settlement the server publishes the seed. Anyone can
// recompute the commitment, re-derive the outcome, and confirm it.
//
// The security property is that the operator must choose its seed before it
// knows the participant set, and cannot change it afterwards without breaking
// a published SHA-256 commitment.
package fair
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/binary"
"encoding/hex"
)
// ServerSeed is the operator's secret contribution to a round, revealed after
// settlement.
type ServerSeed struct {
b [32]byte
}
// NewServerSeed generates a cryptographically random server seed.
func NewServerSeed() ServerSeed {
var s ServerSeed
if _, err := rand.Read(s.b[:]); err != nil {
// A failure of the system CSPRNG is not something to paper over: any
// fallback would silently weaken every outcome derived from it.
panic("fair: system randomness unavailable: " + err.Error())
}
return s
}
// ServerSeedFromBytes reconstructs a seed, for verification of a past round.
func ServerSeedFromBytes(b [32]byte) ServerSeed { return ServerSeed{b: b} }
// Bytes returns the raw seed. Callers must not publish this before settlement.
func (s ServerSeed) Bytes() [32]byte { return s.b }
// Hex renders the seed for the reveal step.
func (s ServerSeed) Hex() string { return hex.EncodeToString(s.b[:]) }
// Commitment is SHA-256 of the seed, published before the round opens.
func (s ServerSeed) Commitment() [32]byte { return sha256.Sum256(s.b[:]) }
// VerifyCommitment reports whether a revealed seed matches a published
// commitment. The comparison is constant-time out of habit; nothing secret
// depends on it by this point, but the cost is zero.
func VerifyCommitment(commitment [32]byte, seed ServerSeed) bool {
actual := seed.Commitment()
return subtle.ConstantTimeCompare(commitment[:], actual[:]) == 1
}
// ClientSeed derives the players' collective contribution from the public keys
// of everyone in the round, in join order. Because the operator cannot control
// who joins, it cannot predict this value when it commits to its own seed.
func ClientSeed(pubkeys [][]byte) [32]byte {
h := sha256.New()
for _, pk := range pubkeys {
// Length-prefix each key so that concatenation is unambiguous and two
// different participant lists cannot hash to the same value.
var n [4]byte
binary.BigEndian.PutUint32(n[:], uint32(len(pk)))
h.Write(n[:])
h.Write(pk)
}
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
// RoundSeed combines both seeds and a nonce into the value that seeds the
// simulation. The nonce separates rounds, or individual plays, that share a
// server seed.
func RoundSeed(server ServerSeed, client [32]byte, nonce uint64) [32]byte {
mac := hmac.New(sha256.New, server.b[:])
mac.Write(client[:])
var n [8]byte
binary.BigEndian.PutUint64(n[:], nonce)
mac.Write(n[:])
var out [32]byte
copy(out[:], mac.Sum(nil))
return out
}
// Proof is everything a player needs to verify one outcome without trusting
// any server response. It is what the verification endpoint returns.
type Proof struct {
Commitment string `json:"commitment"` // published before the round
ServerSeed string `json:"server_seed"` // revealed after settlement
Participants []string `json:"participants"` // hex public keys, join order
Nonce uint64 `json:"nonce"`
RoundSeed string `json:"round_seed"` // derived, shown for convenience
}
// BuildProof assembles the verification record for a settled round.
func BuildProof(server ServerSeed, pubkeys [][]byte, nonce uint64) Proof {
client := ClientSeed(pubkeys)
seed := RoundSeed(server, client, nonce)
participants := make([]string, len(pubkeys))
for i, pk := range pubkeys {
participants[i] = hex.EncodeToString(pk)
}
commitment := server.Commitment()
return Proof{
Commitment: hex.EncodeToString(commitment[:]),
ServerSeed: server.Hex(),
Participants: participants,
Nonce: nonce,
RoundSeed: hex.EncodeToString(seed[:]),
}
}