feat: playable arcade — rooms, identity, client, deployment

Round length is now bounded: the multiplier follows a hyperbolic curve
diverging at 60s, replacing an exponential one where a 275x crash point
produced a two-and-a-half minute round.

Fixes seed reveal, which silently failed every round because pgx cannot
encode a fixed-size byte array as bytea.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
drjones
2026-08-05 15:34:52 +00:00
parent 41c1bb2fdf
commit 2a2a1db8de
17 changed files with 2885 additions and 25 deletions

View File

@@ -0,0 +1,86 @@
package identity_test
import (
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"errors"
"testing"
"github.com/drjones/quantum-arcade/pkg/identity"
)
func newKey(t *testing.T) (string, ed25519.PrivateKey) {
t.Helper()
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
return hex.EncodeToString(pub), priv
}
func TestValidSignatureAuthenticates(t *testing.T) {
a := identity.NewAuthenticator()
pubHex, priv := newKey(t)
nonceHex, err := a.Challenge(pubHex)
if err != nil {
t.Fatal(err)
}
nonce, _ := hex.DecodeString(nonceHex)
sig := ed25519.Sign(priv, nonce)
if err := a.Verify(pubHex, hex.EncodeToString(sig)); err != nil {
t.Fatalf("valid signature rejected: %v", err)
}
}
func TestWrongKeyCannotAuthenticate(t *testing.T) {
a := identity.NewAuthenticator()
pubHex, _ := newKey(t)
_, otherPriv := newKey(t)
nonceHex, _ := a.Challenge(pubHex)
nonce, _ := hex.DecodeString(nonceHex)
sig := ed25519.Sign(otherPriv, nonce)
if err := a.Verify(pubHex, hex.EncodeToString(sig)); !errors.Is(err, identity.ErrBadSignature) {
t.Fatalf("got %v, want ErrBadSignature", err)
}
}
// A captured signature must not work twice.
func TestChallengeIsSingleUse(t *testing.T) {
a := identity.NewAuthenticator()
pubHex, priv := newKey(t)
nonceHex, _ := a.Challenge(pubHex)
nonce, _ := hex.DecodeString(nonceHex)
sigHex := hex.EncodeToString(ed25519.Sign(priv, nonce))
if err := a.Verify(pubHex, sigHex); err != nil {
t.Fatal(err)
}
if err := a.Verify(pubHex, sigHex); !errors.Is(err, identity.ErrUnknownChallenge) {
t.Fatalf("replay succeeded or gave %v, want ErrUnknownChallenge", err)
}
}
func TestMalformedKeyRejected(t *testing.T) {
a := identity.NewAuthenticator()
if _, err := a.Challenge("not-hex"); !errors.Is(err, identity.ErrBadPublicKey) {
t.Fatalf("got %v, want ErrBadPublicKey", err)
}
if _, err := a.Challenge("aabb"); !errors.Is(err, identity.ErrBadPublicKey) {
t.Fatalf("short key: got %v, want ErrBadPublicKey", err)
}
}
func TestVerifyWithoutChallengeFails(t *testing.T) {
a := identity.NewAuthenticator()
pubHex, priv := newKey(t)
sig := ed25519.Sign(priv, []byte("anything"))
if err := a.Verify(pubHex, hex.EncodeToString(sig)); !errors.Is(err, identity.ErrUnknownChallenge) {
t.Fatalf("got %v, want ErrUnknownChallenge", err)
}
}