mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(multiplayer): fence roster topology at persistence
This commit is contained in:
@@ -1406,3 +1406,5 @@ Read-only authenticated queue, proposal, assignment, legacy profile, and ranked-
|
||||
Allocated join admission now retains and applies the signed assignment’s authoritative team and global slot: peer order can no longer rebalance a valid allocation, and inconsistent team/slot claims are rejected before roster admission. The server exposes the verified assignment list for allocation-aware startup and uses the per-team spawn index derived from the assigned slot. Go verification is clean; Godot execution remains blocked by the documented macOS pre-test crash.
|
||||
|
||||
Allocated boot now also validates the complete signed roster shape before opening the gameplay endpoint: malformed claims, duplicate player identities, duplicate slots, and team/global-slot mismatches fail closed rather than leaving a partially usable server. The Godot `--check-only` attempt still reaches the known macOS renderer/ZSTD crash before script parsing, so this startup guard remains statically reviewed and covered by the existing signed-claim tests pending a working Godot runtime.
|
||||
|
||||
The control plane now mirrors that topology fence at roster publication: signed entries with duplicate players, duplicate slots, or a team inconsistent with the canonical global slot are rejected before durable assignment rows are written. Focused store tests cover forged topology and duplicate entries; normal/race Go suites and vet pass.
|
||||
|
||||
@@ -149,11 +149,21 @@ func SaveVerifiedAssignmentRoster(ctx context.Context, db *sql.DB, assignment do
|
||||
}
|
||||
digest := domain.ManifestDigest(assignment.Manifest)
|
||||
rows := make([]DurableAssignment, 0, len(roster))
|
||||
seenPlayers := make(map[string]struct{}, len(roster))
|
||||
seenSlots := make(map[int]struct{}, len(roster))
|
||||
for _, signed := range roster {
|
||||
auth := signed.Authorisation
|
||||
if err := validateSignedRosterEntry(assignment, signed, verify); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exists := seenPlayers[auth.PlayerID]; exists {
|
||||
return fmt.Errorf("invalid signed assignment roster: duplicate player")
|
||||
}
|
||||
if _, exists := seenSlots[auth.Slot]; exists {
|
||||
return fmt.Errorf("invalid signed assignment roster: duplicate slot")
|
||||
}
|
||||
seenPlayers[auth.PlayerID] = struct{}{}
|
||||
seenSlots[auth.Slot] = struct{}{}
|
||||
envelope, err := json.Marshal(signed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode signed assignment roster: %w", err)
|
||||
@@ -172,7 +182,7 @@ func SaveVerifiedAssignmentRoster(ctx context.Context, db *sql.DB, assignment do
|
||||
|
||||
func validateSignedRosterEntry(assignment domain.Assignment, signed domain.SignedJoinAuthorisation, verify func([]byte, []byte) bool) error {
|
||||
auth := signed.Authorisation
|
||||
if len(signed.Signature) == 0 || verify == nil || !verify(domain.JoinAuthorisationBytes(auth), signed.Signature) || auth.MatchID != assignment.Allocation.MatchID || auth.ServerID != assignment.Allocation.ServerID || auth.Protocol != strconv.Itoa(assignment.Allocation.Protocol) || auth.PlayerID == "" || auth.Slot < 0 || auth.Slot > 5 || auth.ExpiresAt.IsZero() {
|
||||
if len(signed.Signature) == 0 || verify == nil || !verify(domain.JoinAuthorisationBytes(auth), signed.Signature) || auth.MatchID != assignment.Allocation.MatchID || auth.ServerID != assignment.Allocation.ServerID || auth.Protocol != strconv.Itoa(assignment.Allocation.Protocol) || auth.PlayerID == "" || auth.Slot < 0 || auth.Slot > 5 || auth.Team < 0 || auth.Team > 1 || auth.Slot/3 != auth.Team || auth.ExpiresAt.IsZero() {
|
||||
return fmt.Errorf("invalid signed assignment roster")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -57,4 +57,14 @@ func TestSignedRosterRequiresCryptographicVerification(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatalf("valid signature rejected: %v", err)
|
||||
}
|
||||
wrongTeam := signed
|
||||
wrongTeam.Authorisation.Slot = 3
|
||||
wrongTeam.Authorisation.Team = 0
|
||||
if err := validateSignedRosterEntry(assignment, wrongTeam, func([]byte, []byte) bool { return true }); err == nil {
|
||||
t.Fatal("team/slot mismatch accepted")
|
||||
}
|
||||
duplicate := signed
|
||||
if err := SaveVerifiedAssignmentRoster(nil, nil, assignment, []domain.SignedJoinAuthorisation{signed, duplicate}, func([]byte, []byte) bool { return true }); err == nil {
|
||||
t.Fatal("duplicate roster player accepted")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user