diff --git a/Game/scripts/server_config.gd b/Game/scripts/server_config.gd index 0867b3e8..f5d4fba7 100644 --- a/Game/scripts/server_config.gd +++ b/Game/scripts/server_config.gd @@ -70,6 +70,8 @@ static func specs() -> Array[Spec]: out.append(Spec.new("match-id", Kind.STRING, "", "allocation", "Opaque allocated match identifier")) out.append(Spec.new("server-id", Kind.STRING, "", "allocation", "Opaque allocated server identifier")) out.append(Spec.new("playlist-version", Kind.STRING, "", "allocation", "Matchmaking playlist contract version")) + out.append(Spec.new("client-build", Kind.STRING, "", "allocation", "Expected immutable client build identifier")) + out.append(Spec.new("assignment-expiry-unix", Kind.INT, 0, "allocation", "Unix expiry for the allocated assignment; must be in the future")) out.append(Spec.new("server-image-digest", Kind.STRING, "", "allocation", "Expected immutable server image digest (sha256:...)")) out.append(Spec.new("transport", Kind.STRING, "", "allocation", "Assigned transport: steam_sdr or enet")) out.append(Spec.new("region", Kind.STRING, "", "allocation", "Assigned region: EU or NA")) @@ -259,9 +261,11 @@ func _validate() -> void: if not rotation in ["sequential", "random"]: errors.append("--arena-rotation must be sequential or random, got '%s'" % rotation) if bool(values["allocated-mode"]): - for key in ["match-id", "server-id", "playlist-version", "server-image-digest", "transport", "region"]: + for key in ["match-id", "server-id", "playlist-version", "client-build", "assignment-expiry-unix", "server-image-digest", "transport", "region"]: if String(values[key]).is_empty(): errors.append("--allocated-mode requires --%s" % key) + if int(values["assignment-expiry-unix"]) <= int(Time.get_unix_time_from_system()): + errors.append("--assignment-expiry-unix must be in the future") var digest := String(values["server-image-digest"]) if not _is_sha256_digest(digest): errors.append("--server-image-digest must be sha256:<64 hex characters>") @@ -307,7 +311,7 @@ static func help_text() -> String: lines.append("") lines.append("The command line overrides the config file, which overrides the defaults") lines.append("shown below. An unknown flag is an error, not a warning.") - var sections := ["general", "network", "match", "logging"] + var sections := ["general", "network", "match", "logging", "allocation"] var all := specs() for section in sections: lines.append("") diff --git a/Game/tests/cases/test_server_config.gd b/Game/tests/cases/test_server_config.gd index 455b5485..14c2cdcf 100644 --- a/Game/tests/cases/test_server_config.gd +++ b/Game/tests/cases/test_server_config.gd @@ -136,7 +136,7 @@ func test_allocated_mode_is_opt_in_and_requires_compatibility_manifest() -> void assert_true(not incomplete.is_valid(), "allocated mode cannot start without its manifest") var valid = _parse([ "--allocated-mode", "--match-id=match_1234567890123456", "--server-id=server_1234567890123456", - "--playlist-version=2026-08-31", "--server-image-digest=sha256:" + "a".repeat(64), + "--playlist-version=2026-08-31", "--client-build=client-2026-08-31", "--assignment-expiry-unix=%d" % (Time.get_unix_time_from_system() + 3600), "--server-image-digest=sha256:" + "a".repeat(64), "--transport=enet", "--region=EU" ]) assert_true(valid.is_valid(), "a complete allocated compatibility manifest is accepted: %s" % str(valid.errors)) @@ -145,7 +145,15 @@ func test_allocated_mode_is_opt_in_and_requires_compatibility_manifest() -> void func test_allocated_mode_rejects_invalid_transport_region_or_digest() -> void: var args := [ "--allocated-mode", "--match-id=m", "--server-id=s", "--playlist-version=v", + "--client-build=client", "--assignment-expiry-unix=%d" % (Time.get_unix_time_from_system() + 3600), "--server-image-digest=sha256:" + "g".repeat(64), "--transport=udp", "--region=AP" ] var config = _parse(args) assert_true(not config.is_valid(), "invalid compatibility values are rejected") + + +func test_allocated_mode_rejects_missing_or_expired_assignment_manifest_fields() -> void: + var missing = _parse(["--allocated-mode", "--match-id=m", "--server-id=s", "--playlist-version=v", "--server-image-digest=sha256:" + "a".repeat(64), "--transport=enet", "--region=EU"]) + assert_true(not missing.is_valid(), "client build and expiry are required") + var expired = _parse(["--allocated-mode", "--match-id=m", "--server-id=s", "--playlist-version=v", "--client-build=client", "--assignment-expiry-unix=1", "--server-image-digest=sha256:" + "a".repeat(64), "--transport=enet", "--region=EU"]) + assert_true(not expired.is_valid(), "expired assignment is rejected") diff --git a/multiplayer-next.md b/multiplayer-next.md index 5e4bf741..31d827cd 100644 --- a/multiplayer-next.md +++ b/multiplayer-next.md @@ -35,19 +35,23 @@ product policy are in [`docs/MATCHMAKING.md`](docs/MATCHMAKING.md). [MATCHMAKING-SLOs.md](docs/MATCHMAKING-SLOs.md). - [x] Publish versioned OpenAPI/WebSocket contracts, stable IDs, legal state transitions, revisions and idempotency semantics ([v1 contracts](server/contracts/v1/)). -- [ ] Add PostgreSQL queue ownership/active-participation fences, durable - domain migrations/outbox and Redis indexes/TTLs; lost Redis writes must not - split a proposal or corrupt durable state. -- [ ] Define assignment compatibility and opt-in `ServerConfig` flags whose - defaults reproduce the community-server path. +- [ ] **IN PROGRESS:** Add PostgreSQL queue ownership/active-participation + fences, durable domain migrations/outbox and Redis indexes/TTLs; lost Redis + writes must not split a proposal or corrupt durable state. Initial migration + and serializable store boundaries are implemented; live DB/cache repair gates remain. +- [ ] **IN PROGRESS:** Define assignment compatibility and opt-in `ServerConfig` + flags whose defaults reproduce the community-server path. Allocation manifest + validation now covers client build and future expiry; signed admission remains. ## Phase 8 — identity and security - [ ] Validate Steam Web API tickets only in the secure backend; issue revocable sessions and reconnect-safe match/identity/slot authorisations with server-owned connection-generation fencing. -- [ ] Authenticate results with pod/GameServer-bound workload identity; make - identical duplicates idempotent and conflicting results inert/alerting. +- [ ] **IN PROGRESS:** Authenticate results with pod/GameServer-bound workload + identity; make identical duplicates idempotent and conflicting results + inert/alerting. Pure Go binding, hashing, reconciliation, and SQL boundaries exist; + production credential validation remains. - [ ] Complete the threat model for forgery, replay, queue/flood/bot abuse, workload/insider compromise, DDoS, supply chain and denial-of-wallet. - [ ] Enforce restricted workloads/RBAC/networks/private stores/backups/secrets; @@ -58,23 +62,23 @@ product policy are in [`docs/MATCHMAKING.md`](docs/MATCHMAKING.md). ## Phase 8 — queues, playlists and rating -- [ ] Add one PostgreSQL-owned queue ticket/player with 10 s heartbeat, 30 s - expiry, Redis candidate cache and restart/failover repair. -- [ ] Validate opaque Steam ping locations and nonce-bound probes server-side; +- [ ] **IN PROGRESS:** Add one PostgreSQL-owned queue ticket/player with 10 s + heartbeat, 30 s expiry, Redis candidate cache and restart/failover repair. +- [ ] **IN PROGRESS:** Validate opaque Steam ping locations and nonce-bound probes server-side; require <=100 ms, enforce discrepancy quarantine and the locked widening/ region/team tie-break rules. -- [ ] Send 10 s proposals to every selected human: ranked six, relaxed casual +- [ ] **IN PROGRESS:** Send 10 s proposals to every selected human: ranked six, relaxed casual two to six with disclosed bots; enforce exact cooldown and queue-precedence behavior. -- [ ] Fence proposals/participants in a PostgreSQL serializable transaction; +- [ ] **IN PROGRESS:** Fence proposals/participants in a PostgreSQL serializable transaction; prove loss of an acknowledged Redis write cannot split players. - [ ] Casual: target 3v3 humans, after 60 s allow >=2 humans (one/team) plus bots, kickoff-only human backfill and no backfill loss/decline penalty. -- [ ] Ranked: exactly six humans, solo-only, no bots/backfill, random-enabled - non-elevated arenas only, 60 s reconnect grace and escalating abandons. -- [ ] Implement the documented exact Glicko-2 equations, fractional 3v3 +- [ ] **IN PROGRESS:** Ranked: exactly six humans, solo-only, no bots/backfill, + random-enabled non-elevated arenas only, 60 s reconnect grace and escalating abandons. +- [ ] **IN PROGRESS:** Implement the documented exact Glicko-2 equations, fractional 3v3 weights, inactivity/update locking/golden vectors and ten provisional games. -- [ ] Add ranked-only exactly-once 12-week soft seasons; distinguish retryable +- [ ] **IN PROGRESS:** Add ranked-only exactly-once 12-week soft seasons; distinguish retryable result-delivery outages from match-integrity failures and rating exemptions. ## Phase 8 — Agones and regional server capacity diff --git a/multiplayer-todo.md b/multiplayer-todo.md index 69d72551..f3f81fc9 100644 --- a/multiplayer-todo.md +++ b/multiplayer-todo.md @@ -1173,7 +1173,7 @@ the local/CI/community transport, not a silent production fallback. | 8.3 `[D:8.1]` | **DONE.** Publish versioned OpenAPI + WebSocket contracts for Steam login/session, profile/rating, queue create/heartbeat/cancel/resume, proposal accept/decline, assignment/status, server registration/roster/result/shutdown | [`server/contracts/v1/`](server/contracts/v1/) contains machine-readable REST/events contracts and dependency-free structural tests; REST resync is specified by the contract | | 8.4 `[D:8.3]` | **DONE.** Define opaque IDs, legal queue/match state transitions, revisions and idempotency keys | [`server/contracts/v1/state-transitions.json`](server/contracts/v1/state-transitions.json) locks terminal states, legal edges, stale-revision handling and same-key replay/conflict behavior; contract tests cover the invariants | | 8.5 `[D:8.4]` | **IN PROGRESS.** Initial PostgreSQL migration now defines durable idempotency keys, queue ownership/active-participation fencing, identities, sessions/revocations, ranked seasons, ratings/events, matches/participants, penalties, results, audits and outbox | `server/migrations/0001_initial.sql` and static checks cover the durable tables, uniqueness/check constraints and Redis-as-cache boundary; live PostgreSQL up/rollback/forward migration, serializable adapters and cache-loss repair remain | -| 8.6 `[D:8.3,8.4]` | **IN PROGRESS.** Add allocated-mode `ServerConfig` compatibility fields as opt-in defaults | `ServerConfig` now validates allocation mode, match/server IDs, playlist version, image digest, transport and EU/NA region; client-build/expiry/signed-authorisation admission and full manifest tests remain | +| 8.6 `[D:8.3,8.4]` | **IN PROGRESS.** Add allocated-mode `ServerConfig` compatibility fields as opt-in defaults | `ServerConfig` now validates allocation mode, match/server IDs, playlist version, client build, future assignment expiry, image digest, transport and EU/NA region; signed-authorisation admission and full manifest/runtime tests remain | #### 8B — Authentication and secure control plane