package fair_test import ( "bytes" "crypto/sha256" "encoding/hex" "testing" "github.com/drjones/quantum-arcade/pkg/fair" ) func TestCommitmentHidesSeed(t *testing.T) { s := fair.NewServerSeed() c := s.Commitment() raw := s.Bytes() if bytes.Contains(c[:], raw[:8]) { t.Fatal("commitment leaks seed bytes") } } func TestCommitmentVerifies(t *testing.T) { s := fair.NewServerSeed() c := s.Commitment() if !fair.VerifyCommitment(c, s) { t.Fatal("valid seed failed its own commitment") } other := fair.NewServerSeed() if fair.VerifyCommitment(c, other) { t.Fatal("a different seed satisfied the commitment") } } func TestClientSeedDependsOnEveryParticipant(t *testing.T) { a := []byte("player-a-pubkey") b := []byte("player-b-pubkey") c := []byte("player-c-pubkey") withAll := fair.ClientSeed([][]byte{a, b, c}) withoutC := fair.ClientSeed([][]byte{a, b}) if withAll == withoutC { t.Fatal("removing a participant did not change the client seed") } } // Join order must matter in a defined way, but the same set in the same order // must always produce the same seed. func TestClientSeedIsStable(t *testing.T) { keys := [][]byte{[]byte("k1"), []byte("k2")} if fair.ClientSeed(keys) != fair.ClientSeed(keys) { t.Fatal("client seed is not stable for identical input") } } func TestRoundSeedIsDeterministic(t *testing.T) { s := fair.NewServerSeed() cs := fair.ClientSeed([][]byte{[]byte("p1")}) first := fair.RoundSeed(s, cs, 7) for i := 0; i < 50; i++ { if fair.RoundSeed(s, cs, 7) != first { t.Fatal("round seed is not deterministic") } } } func TestNonceSeparatesOutcomes(t *testing.T) { s := fair.NewServerSeed() cs := fair.ClientSeed([][]byte{[]byte("p1")}) seen := map[[32]byte]bool{} for n := uint64(0); n < 1000; n++ { seed := fair.RoundSeed(s, cs, n) if seen[seed] { t.Fatalf("nonce %d collided with an earlier round seed", n) } seen[seed] = true } } // The full protocol as a player would check it: the commitment published before // the round must match the seed revealed after, and the seed must reproduce the // outcome. func TestEndToEndVerification(t *testing.T) { server := fair.NewServerSeed() published := server.Commitment() participants := [][]byte{[]byte("alice"), []byte("bob")} cs := fair.ClientSeed(participants) const nonce = 42 seed := fair.RoundSeed(server, cs, nonce) // After the round the server reveals the seed. A player recomputes: if !fair.VerifyCommitment(published, server) { t.Fatal("revealed seed does not match published commitment") } recomputed := fair.RoundSeed(server, fair.ClientSeed(participants), nonce) if recomputed != seed { t.Fatal("independent recomputation produced a different seed") } } func TestServerSeedsAreUnique(t *testing.T) { seen := map[[32]byte]bool{} for i := 0; i < 1000; i++ { s := fair.NewServerSeed() if seen[s.Bytes()] { t.Fatal("NewServerSeed returned a duplicate") } seen[s.Bytes()] = true } } func TestServerSeedRoundTripsThroughBytes(t *testing.T) { original := fair.NewServerSeed() restored := fair.ServerSeedFromBytes(original.Bytes()) if restored.Bytes() != original.Bytes() { t.Fatal("seed did not survive a byte round trip") } if restored.Commitment() != original.Commitment() { t.Fatal("restored seed produces a different commitment") } if restored.Hex() != original.Hex() { t.Fatal("restored seed renders differently") } } func TestHexIsFullLength(t *testing.T) { s := fair.NewServerSeed() if len(s.Hex()) != 64 { t.Fatalf("hex seed is %d characters, want 64", len(s.Hex())) } } func TestBuildProofIsSelfConsistent(t *testing.T) { server := fair.NewServerSeed() keys := [][]byte{[]byte("alice"), []byte("bob"), []byte("carol")} const nonce = 17 proof := fair.BuildProof(server, keys, nonce) if proof.Nonce != nonce { t.Fatalf("proof nonce = %d, want %d", proof.Nonce, nonce) } if len(proof.Participants) != len(keys) { t.Fatalf("proof lists %d participants, want %d", len(proof.Participants), len(keys)) } // Every value in the proof must be reproducible from the others. seedBytes, err := hex.DecodeString(proof.ServerSeed) if err != nil { t.Fatal(err) } sum := sha256.Sum256(seedBytes) if hex.EncodeToString(sum[:]) != proof.Commitment { t.Fatal("proof commitment does not match its own seed") } var restored [32]byte copy(restored[:], seedBytes) want := fair.RoundSeed(fair.ServerSeedFromBytes(restored), fair.ClientSeed(keys), nonce) if hex.EncodeToString(want[:]) != proof.RoundSeed { t.Fatal("proof round seed does not follow from its inputs") } } func TestProofParticipantsPreserveOrder(t *testing.T) { server := fair.NewServerSeed() keys := [][]byte{[]byte("first"), []byte("second")} proof := fair.BuildProof(server, keys, 1) if proof.Participants[0] != hex.EncodeToString(keys[0]) { t.Fatal("participant order was not preserved") } if proof.Participants[1] != hex.EncodeToString(keys[1]) { t.Fatal("participant order was not preserved") } } // Reordering the same players must change the seed, since order is part of the // commitment. Otherwise a player could be swapped in without detection. func TestParticipantOrderAffectsTheSeed(t *testing.T) { a, b := []byte("alice"), []byte("bob") if fair.ClientSeed([][]byte{a, b}) == fair.ClientSeed([][]byte{b, a}) { t.Fatal("reordering participants did not change the client seed") } } // Length-prefixing must prevent two different participant lists from colliding // through simple concatenation. func TestClientSeedResistsConcatenationCollisions(t *testing.T) { // Without length prefixes, {"ab","c"} and {"a","bc"} would hash the same. one := fair.ClientSeed([][]byte{[]byte("ab"), []byte("c")}) two := fair.ClientSeed([][]byte{[]byte("a"), []byte("bc")}) if one == two { t.Fatal("different participant lists collided; length prefixing is broken") } } func TestEmptyParticipantListIsStable(t *testing.T) { if fair.ClientSeed(nil) != fair.ClientSeed([][]byte{}) { t.Fatal("nil and empty participant lists disagree") } } // A round with no players must still produce a valid, verifiable outcome. func TestRoundWithNoPlayersStillVerifies(t *testing.T) { server := fair.NewServerSeed() commitment := server.Commitment() proof := fair.BuildProof(server, nil, 5) if !fair.VerifyCommitment(commitment, server) { t.Fatal("empty round does not verify") } if proof.RoundSeed == "" { t.Fatal("empty round produced no seed") } }