// 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[:]), } }