docs: record the closed root blocker and the new probe/rotation contract

docs/MATCHMAKING.md is the stated source of truth for this design, so it
changes first: the probe challenge endpoint and why probing gates
matching rather than merely improving it, and the key-ID rotation
procedure that makes overlapping-key rotation concrete.

multiplayer-next.md §0's root blocker is closed rather than deleted --
what it was, why it blocked everything, and how it was resolved, since
the reasoning is what a future reader needs. Tasks 7.6, 8.15 and 8.31
updated to what actually remains, which in every case is now external
rather than unbuilt.

TODO.md #14 asked for a join-signing design decision; that decision is
recorded with its rationale. CLAUDE.md no longer says a real deployment
cannot complete a match end to end.
This commit is contained in:
Josh Creek
2026-09-05 11:00:50 +01:00
parent f628ccfd35
commit ccf7d0fbfe
4 changed files with 56 additions and 21 deletions
+32 -18
View File
@@ -41,21 +41,32 @@ blocker and is in progress.** It is larger than anything below and adds a
backend service outside the Godot project. Tasks are in §7; the design is in
[`docs/MATCHMAKING.md`](docs/MATCHMAKING.md).
**The current root blocker** ([#14](https://github.com/jcreek/CosmicClash/issues/14)): nothing in production ever publishes a
player's signed match assignment. `store.SaveAssignment`/`SaveAssignments`/
`SaveVerifiedAssignmentRoster` and `allocator.Service.PublishRoster` are
fully built and tested in isolation, but no real code path
(`allocator/worker.go`, `cmd/allocator`) ever calls them — only tests do, by
seeding the table directly rather than exercising the real write path. Since
**The former root blocker** ([#14](https://github.com/jcreek/CosmicClash/issues/14)) **is closed.** Nothing in production
used to publish a player's signed match assignment:
`store.SaveVerifiedAssignmentRoster` and `allocator.Service.PublishRoster`
were fully built and tested in isolation, but no real code path called them —
only tests did, by seeding the table directly. Since
`AdvanceServerRegistration`'s SQL requires an `assignments` row per
participant before a match can reach `ASSIGNMENT_READY`, **a real deployment
cannot advance any match past `PROCESS_READY`** — no player can ever receive
a real assignment or connect, regardless of how correct every other piece
(including the client-side connect-wiring in task 8.41) is. See task 8.31
for the full detail. Closing it needs new security-relevant design (a
join-signing key shared between allocator and game server, roster-digest
computation, per-player authorisation construction) — flagged rather than
built, at the user's explicit direction, pending a decision on that design.
participant before a match can reach `ASSIGNMENT_READY`, a real deployment
could not advance any match past `PROCESS_READY`.
`allocator.Worker.RunOnce` now builds one signed join authorisation per
durable participant and publishes the roster after binding the allocation, and
`cmd/allocator` refuses to start without key material rather than stranding
every match silently. The signing-key design that was pending a decision is
settled: HMAC-SHA256 over the canonical claim bytes, with a **key ID** in
those bytes so allocated servers can hold the set of currently-valid keys and
rotation does not invalidate authorisations already issued for in-flight
matches. See `docs/MATCHMAKING.md` §2 for the rotation procedure.
Two further blockers of the same shape were found and closed alongside it:
regional RTT probing had no nonce-issuing endpoint (so no client-created
ticket could ever be selected — the matcher requires non-empty RTT evidence),
and the Kubernetes base deployed a control-plane image nothing built while
building a matcher image nothing deployed. What remains for a live deployment
is external: a Steamworks App ID and publisher key ([#15](https://github.com/jcreek/CosmicClash/issues/15)),
custom GodotSteam builds ([#16](https://github.com/jcreek/CosmicClash/issues/16)),
and a real cluster ([#17](https://github.com/jcreek/CosmicClash/issues/17)).
### Blocking sign-off — the work exists, the verification does not
@@ -141,7 +152,7 @@ retrofitting one.
| 7.3 `[D:7.2]` `[P]` | Server-browser UI and `ISteamMatchmakingServers` adapter | Unimplemented until real Steam SDK/API access is available; ENet direct-IP remains the supported browser-free path meanwhile |
| 7.4 `[D:7.2]` `[P]` | Auth tickets in `hello``BeginAuthSession`, Steam identity in the roster, persistent ban list | Real GodotSteam auth integration, server-side VAC state, durable ban storage remain. **Fixes known defect C** for direct/community servers once landed |
| 7.5 `[D:7.2]` `[P]` | `SteamBootstrap` gating (stock builds keep ENet, explicit Steam selection fails closed) | Custom Steam client/server export smoke remains blocked on pinned GodotSteam binaries |
| 7.6 `[D:7.4]` | Backend `AuthCoordinator`, session persistence, `ControlPlaneClient.login_steam()` | Real Steam `BeginAuthSession`/`EndAuthSession` adapter, login UI, live PostgreSQL/session integration remain |
| 7.6 `[D:7.4]` | Backend `AuthCoordinator`, session persistence, `ControlPlaneClient.login_steam()`, real `ISteamUserAuth/AuthenticateUserTicket` adapter (`server/steam`), client web-API ticket acquisition, sign-in before matchmaking | Needs a real App ID and publisher key ([#15](https://github.com/jcreek/CosmicClash/issues/15)) and a custom GodotSteam build ([#16](https://github.com/jcreek/CosmicClash/issues/16)) to exercise live; sign-in is config-gated and returns 503 until both are set |
| 7.7 `[D:7.1]` `[P]` | Obtain the production App ID, publisher key, SDR coordinator SDK/signing approval, certificates and hosted-data-centre support from Valve | Not started |
| 7.8 `[D:7.6,7.7]` | Ticketed Hosted Dedicated Server SDR: routing registration, coordinator-issued player→server relay tickets, client ticket installation, reconnect and expiry | Not started; depends on 7.6 and 7.7 |
@@ -154,7 +165,10 @@ the largest architectural departure in the project's history; read the
design doc before picking up any task below. The local control-plane,
durable-store, allocated-server, and verification paths are substantially
implemented; every row below lists only what's still open, not what's
built. **The critical path is task 8.31 — see §0's root blocker.**
built. **Task 8.31, formerly the critical path, is done — see §0.** What now
gates a live deployment is external: an App ID ([#15](https://github.com/jcreek/CosmicClash/issues/15)),
GodotSteam builds ([#16](https://github.com/jcreek/CosmicClash/issues/16)), and a
cluster ([#17](https://github.com/jcreek/CosmicClash/issues/17)).
**Hard dependency on 7.6 and 7.8.** The local allocated path binds slot
reclaim to a control-plane-signed player identity and locks its team/slot
@@ -196,7 +210,7 @@ are done; everything below is what's left on the tasks still open.
| # | Task | Remaining |
|---|---|---|
| 8.14 `[D:8.4,8.5,8.8]` | Queue policy (ownership, heartbeat/expiry, candidate projection) | Live Redis failover-under-load and worker integration remain |
| 8.15 `[D:7.8,8.3]` | Probe validation (RTT, nonce/freshness/region, quarantine) | Steam coordinator, regional probe adapters, multi-region probe population remain |
| 8.15 `[D:7.8,8.3]` | Probe validation (RTT, nonce/freshness/region, quarantine), `POST /v1/probes/{region}/challenge`, durable single-use nonces, client probe collection before queueing, candidate-index refresh after probe | Steam coordinator ping-location source remains (a placeholder blob is sent without a Steam runtime); multi-region endpoint deployment remains |
| 8.16 `[D:8.14,8.15]` | Candidate/team formation, matcher worker | Long-running worker integration remains |
| 8.17 `[D:8.14,8.16]` | Proposal policy (response window, cooldowns, offender/innocent split) | Live PostgreSQL execution and allocation integration remain |
| 8.18 `[D:8.5,8.14,8.17]` | Store layer (serializable retries, claim SQL, atomic promotion) | Allocation runtime integration remains |
@@ -217,7 +231,7 @@ are done; everything below is what's left on the tasks still open.
| 8.28 `[D:8.6,8.27]` | Process-ready/Agones-Ready separation, control-plane registration | Remaining gates are live Agones annotation/shutdown behavior and production cluster readiness — see task 8.49 |
| 8.29 `[D:8.26,8.27]` | Dynamic port/SDR env propagation | Real Agones dynamic/passthrough mapping, POP/cert/firewall/NAT, multi-match fixture remain |
| 8.30 `[D:8.18,8.26,8.28,8.29]` | Allocation leasing, compatibility validation | Full unknown-outcome cluster recovery and signed roster metadata remain |
| **8.31** `[D:8.9,8.30]` | Signed assignment/roster persistence, player recovery | **This is the actual root blocker of the whole allocation-to-connect pipeline (see §0).** `store.SaveAssignment`/`SaveAssignments`/`SaveVerifiedAssignmentRoster` and `allocator.Service.PublishRoster` are built and tested but never called from `allocator/worker.go`, `cmd/allocator`, or anywhere else in production — only tests seed the table directly. A real match cannot advance past `PROCESS_READY`. Closing it needs new security-relevant design: a join-signing key shared between the allocator (to sign) and the game server (`fleet.yaml` already mounts one for verification via `--join-authorisations-key-file`, but no control-plane binary has a matching signing flag), roster-digest computation, and per-player `domain.JoinAuthorisation` construction via the already-built `domain.SignJoinAuthorisationHMAC`. Flagged rather than fixed at the user's explicit direction, pending a decision on that design |
| **8.31** `[D:8.9,8.30]` | Signed assignment/roster persistence, player recovery | **Done — this was the root blocker of the allocation-to-connect pipeline.** `allocator.Worker.RunOnce` now builds one signed join authorisation per durable participant and calls `PublishRoster` after binding; `cmd/allocator` takes `--join-authorisations-key-file`/`--join-authorisations-key-id` and refuses to start without them. The signing design is settled: HMAC-SHA256 over the canonical claim bytes with a key ID inside them, so servers hold a key *set* and rotation does not invalidate in-flight matches. The provider endpoint is now persisted on the allocation so a worker crashing between allocating and publishing can retry. Verified by an integration test that drives the real worker through the supervisor's own roster read path without seeding `assignments`. Live Agones verification remains ([#17](https://github.com/jcreek/CosmicClash/issues/17)) |
| 8.32 `[D:8.2,8.26,8.30]` | FleetAutoscaler baseline, Ready buffer | Regional on-demand node pools/failure domains, pre-pull rollout, warm-allocation p95/p99, N+1 certification remain |
| 8.33 `[D:8.26,8.32]` | Fleet scheduling, zone spread | Regional node pools, forced node-loss testing, measured N+1 headroom remain |
| 8.34 `[D:8.28,8.29]` | Native x86_64 benchmark of boot-to-process-ready/assignment-ready, p99 CPU/RSS/network, node cap with 30% headroom | Not started |