873 lines
28 KiB
Plaintext
873 lines
28 KiB
Plaintext
# Ping-Pong PvP 1v1 - Full Technical Architecture Audit (Node.js + Redis + WebSocket)
|
|
|
|
Data audytu: 2026-05-20
|
|
Zakres: pełny statyczny audyt kodu (bez refaktoru i bez zmian architektury na tym etapie)
|
|
Tryb: production-grade technical assessment
|
|
|
|
## Zakres przeanalizowanych komponentów
|
|
|
|
- Node server: `public_html/disciplines/ping-pong/1v1/node-server/src/*`
|
|
- Klient gry WebSocket: `public_html/disciplines/ping-pong/1v1/js/online.js`
|
|
- Strona wejściowa gry: `public_html/disciplines/ping-pong/1v1/index.php`
|
|
- API PHP (ticket/status/rewards): `public_html/api/matches/ping-pong/1v1/*`
|
|
- Internal HMAC/ticket/env helpers: `public_html/api/matches/ping-pong/1v1/internal/*`
|
|
- CRON rewards worker: `public_html/cron/process_rewards_jobs.php`
|
|
- Session/auth bootstrap: `public_html/includes/session_bootstrap.php`
|
|
- PM2 deployment config: `public_html/disciplines/ping-pong/1v1/node-server/ecosystem.config.cjs`
|
|
|
|
## Metodologia
|
|
|
|
- Audyt oparty wyłącznie na kodzie źródłowym i aktualnych artefaktach repo.
|
|
- Brak założeń "na wiarę" o infrastrukturze poza tym, co jest jawnie zaimplementowane.
|
|
- Brak zmian kodu produkcyjnego (zgodnie z wymaganiem).
|
|
|
|
---
|
|
|
|
# 1. OGOLNA ARCHITEKTURA
|
|
|
|
## 1.1 Mapa systemu (as-is)
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[Browser Client online.js] -->|GET ticket| B[PHP /api/matches/ping-pong/1v1/ticket.php]
|
|
B -->|signed short-lived ticket| A
|
|
A -->|WebSocket hello/queue/input| C[Node 1v1 Server index.js]
|
|
C -->|queue, snapshots, worker ownership| D[(Redis)]
|
|
C -->|match rows + ticks + direct rewards| E[(MySQL)]
|
|
C -->|fallback HTTP rewards signed HMAC| F[PHP /api/matches/ping-pong/1v1/index.php]
|
|
F -->|rewards_jobs status| E
|
|
A -->|poll rewards job| G[PHP /api/matches/ping-pong/1v1/status.php]
|
|
G --> E
|
|
H[PM2 cluster mode] --> C
|
|
C -->|cross-worker WS routing| D
|
|
```
|
|
|
|
## 1.2 Zaleznosci modulow
|
|
|
|
- `index.js` jest orchestrator-em runtime:
|
|
- auth ticket (`ticket.js`)
|
|
- transport (`server.js`)
|
|
- queue (`matchmaking.js`)
|
|
- physics (`physics.js`)
|
|
- redis storage (`redisClient.js`, `matchStore.js`)
|
|
- cross-worker IPC (`ipc.js`)
|
|
- persistence/economy (`mysqlWriter.js`, fallback `rewardsClient.js`)
|
|
|
|
- PHP warstwa dostarcza:
|
|
- issuance ticketu WebSocket (`ticket.php`)
|
|
- fallback settlement (`index.php`)
|
|
- polling statusu settlementu (`status.php`)
|
|
- profile snapshot do UI (`player-summary.php`)
|
|
|
|
## 1.3 Lifecycle requestow i sesji
|
|
|
|
1. Uzytkownik otwiera `/disciplines/ping-pong/1v1/`.
|
|
2. Front pobiera ticket GET (`ticket.php`) na bazie sesji PHP.
|
|
3. Front otwiera WS i wysyla `hello` z ticketem.
|
|
4. Node waliduje HMAC ticketu i TTL.
|
|
5. User moze wyslac `queue.join`.
|
|
6. Matchmaking loop dobiera pare i tworzy obiekt `Match`.
|
|
7. Match emituje `match.found`, potem cykliczne `match.state`.
|
|
8. Klient wysyla `match.input` (33 ms) i app-level `ping` (3 s).
|
|
9. Koniec meczu: `match.end`, snapshot finalny, settlement DB (direct), fallback HTTP gdy direct fail.
|
|
10. Front opcjonalnie polluje `status.php` dla `rewards_jobs`.
|
|
|
|
## 1.4 Lifecycle meczu (dokladny)
|
|
|
|
- Warmup 10 s + pre-start break 3 s.
|
|
- Faza gry: tick server-authoritative `step()`.
|
|
- Point pause 1 s po zdobyciu punktu.
|
|
- Set break 3 s przy zakonczeniu seta.
|
|
- Best of 5 (setsToWin=3), set do 11 z przewaga 2.
|
|
- Match ending reasons:
|
|
- `sets`
|
|
- `forfeit_left` / `forfeit_right`
|
|
- `both_disconnect`
|
|
- `disconnect_timeout_left` / `disconnect_timeout_right`
|
|
|
|
## 1.5 Przeplyw danych
|
|
|
|
- Sterowanie: client -> WS `match.input` -> `Match.onInput` -> physics tick.
|
|
- Stan: server -> WS `match.state` (broadcast co tick).
|
|
- Reconnect: Redis snapshot (`match:{matchId}`) + IPC ownership map.
|
|
- Economy: Node direct MySQL transaction albo fallback signed HTTP do PHP.
|
|
|
|
## 1.6 Flow uzytkowania od wejscia do konca meczu
|
|
|
|
- Wejscie i walidacja username/suspension po stronie PHP page bootstrap.
|
|
- Ticket oparty o aktywna sesje PHP.
|
|
- Matchmaking przez Redis ZSET.
|
|
- Gameplay w Node (authoritative physics + score).
|
|
- Settlement rewards i statystyk.
|
|
- Powrot do lobby po animacji post-match.
|
|
|
|
## 1.7 Najwazniejsze obserwacje architektoniczne
|
|
|
|
- Architektura jest hybrydowa Node+PHP i dziala, ale ma duzy coupling w obszarze economy.
|
|
- Istnieja dwa niezalezne pathy rewardow z roznymi stawkami winnera.
|
|
- Cluster PM2 jest oparty o Redis IPC, ale Redis fallback do in-memory pozostaje aktywny i zmienia semantyke systemu rozproszonego.
|
|
|
|
---
|
|
|
|
# 2. MATCHMAKING
|
|
|
|
## 2.1 Jak gracze sa dobierani
|
|
|
|
- Queue: Redis ZSET `pp:1v1:queue:zset`.
|
|
- `enqueue` robi `zAdd` z `score=Date.now()`.
|
|
- Co 150 ms odpalany jest `dequeuePair`.
|
|
- `dequeuePair`:
|
|
- lock globalny `queue:lock` (`SET NX PX 200`)
|
|
- `zRange(0, -1)` po calej kolejce
|
|
- losowe dwa indexy
|
|
- `zRem` obu graczy
|
|
|
|
## 2.2 Bezpieczenstwo matchmakingu
|
|
|
|
Co jest OK:
|
|
|
|
- ZSET trzyma unikalny `userId` (brak wielokrotnych wpisow tej samej wartosci).
|
|
- Basic lock redukuje herd effect miedzy workerami.
|
|
|
|
Co jest ryzykowne:
|
|
|
|
- Globalny lock i pelny scan kolejki co 150 ms (O(n)) sa bottleneckiem skali.
|
|
- Lock TTL 200 ms moze wygasac podczas wolnych operacji (risk overlap).
|
|
- Brak atomowej logiki pairingu po stronie Redis (Lua/transaction script).
|
|
|
|
## 2.3 Race conditions i exploity
|
|
|
|
Krytyczne:
|
|
|
|
- Lost players po dequeue gdy walidacja username fail:
|
|
- para jest juz usunieta z kolejki
|
|
- przy `!leftUsername || !rightUsername` funkcja `return` bez requeue
|
|
- efekt: user znika z kolejki bez meczu (ghost/lost queue)
|
|
|
|
- Stale ownership keys (remote alive false-positive):
|
|
- dla remote owner `alive = !!owner` bez heartbeat worker liveness
|
|
- crash workera + TTL key -> matchmaking moze uznac gracza za aktywnego
|
|
- rezultat: dead/ghost match
|
|
|
|
- Queue leave vs dequeue race:
|
|
- gracz moze wyslac `queue.leave` gdy jest juz pobrany do pary
|
|
- brak finalnego potwierdzenia uczestnictwa przed utworzeniem Match
|
|
|
|
Wysokie:
|
|
|
|
- Brak MMR/skill/fairness, dobieranie losowe.
|
|
- Brak anti-abuse throttle na `queue.join` spam.
|
|
|
|
## 2.4 Podwojne matchowanie i stuck/ghost/dead przypadki
|
|
|
|
- Podwojne matchowanie (tego samego usera):
|
|
- niskie ryzyko na poziomie queue (ZSET unique)
|
|
- ale ryzyko istnieje po stronie reconnect (patrz sekcja 6), gdzie user moze wejsc ponownie do queue bez restore `session.matchId`
|
|
|
|
- Stuck queue:
|
|
- mozliwe przy stale state i lost dequeue path.
|
|
|
|
- Ghost queue:
|
|
- mozliwe po crash workerow i nieswiezych map ownership.
|
|
|
|
- Dead match:
|
|
- mozliwy przez stale ownership i cross-worker routing do niezyjacego workera.
|
|
|
|
- Duplicate match:
|
|
- low/medium ryzyko z uwagi na lock + unikalny userId w ZSET.
|
|
- wzrasta gdy lock TTL jest zbyt krótki vs latency.
|
|
|
|
## 2.5 Skalowalnosc matchmakingu (1k/10k/100k+ CCU)
|
|
|
|
1k CCU:
|
|
- prawdopodobnie dziala, ale z ryzykiem jitter i okazjonalnych race.
|
|
|
|
10k CCU:
|
|
- pelny `zRange(0,-1)` co 150 ms staje sie kosztowny.
|
|
- lock contention i redis CPU widoczne.
|
|
|
|
100k+ CCU:
|
|
- obecny algorytm nie jest wystarczajacy.
|
|
- global queue scan + global lock nie skaluja horyzontalnie.
|
|
|
|
## 2.6 Bottlenecks i distributed systems issues
|
|
|
|
- Jedna globalna kolejka + jeden lock.
|
|
- Brak shardingu/bucketow kolejki.
|
|
- Brak atomowego pairing script.
|
|
- Semantyka alive oparta na key presence, nie na real heartbeat worker/session.
|
|
|
|
---
|
|
|
|
# 3. WEBSOCKET ARCHITECTURE
|
|
|
|
## 3.1 Mapa eventow (kto, kiedy, co zmienia)
|
|
|
|
### Client -> Server
|
|
|
|
- `hello`
|
|
- owner: klient po open
|
|
- state: inicjalizuje session usera
|
|
- risk: replay ticket w oknie TTL
|
|
|
|
- `queue.join`
|
|
- owner: klient lobby
|
|
- state: queue zset add
|
|
- risk: spam bez rate limit
|
|
|
|
- `queue.leave`
|
|
- owner: klient lobby
|
|
- state: queue zset rem
|
|
|
|
- `match.input`
|
|
- owner: klient w meczu
|
|
- state: `players[side].input`
|
|
- risk: packet spam/CPU abuse
|
|
|
|
- `ping`
|
|
- owner: klient w meczu
|
|
- state: heartbeat + opponent ping forwarding
|
|
|
|
- `match.leave`
|
|
- owner: klient przy wyjsciu
|
|
- state: forfeit path
|
|
- risk: frame-close race
|
|
|
|
### Server -> Client
|
|
|
|
- `hello`
|
|
- handshake prompt
|
|
|
|
- `hello.ok` / `hello.error`
|
|
- auth result
|
|
|
|
- `queue.status`
|
|
- searching/idle + queue size
|
|
|
|
- `match.found`
|
|
- tworzy lokalny match context
|
|
|
|
- `match.reconnected`
|
|
- restore side/opponent/match metadata
|
|
|
|
- `match.snapshot`
|
|
- snapshot restore/final state hint
|
|
|
|
- `match.state`
|
|
- authoritative game state stream
|
|
|
|
- `match.set_break`
|
|
- break countdown signal
|
|
|
|
- `match.end`
|
|
- final payload + reason
|
|
|
|
- `rewards.done` / `rewards.queued` / `rewards.error`
|
|
- settlement status
|
|
|
|
- `pong`
|
|
- RTT update
|
|
|
|
- `opponent.ping`
|
|
- przeciwnik latency info
|
|
|
|
- `opponent.status`
|
|
- connected/disconnected signal
|
|
|
|
## 3.2 Kolejnosc i ownership
|
|
|
|
- Ownership sesji usera jest trzymany przez `userSockets` + Redis key `ws:w:{userId}`.
|
|
- Ownership meczu przez `match:w:{matchId}`.
|
|
- Cross-worker events routeowane Redis Pub/Sub (`ipc:w{worker}`).
|
|
|
|
## 3.3 Duplicate event risks
|
|
|
|
- Brak idempotency keys dla eventow gameplay.
|
|
- `match.input` nie uzywa `seq` do deduplikacji/reorder control.
|
|
- UI moze dostac stale kombinacje `match.snapshot` + `match.state` z roznych workerow.
|
|
|
|
## 3.4 Race conditions i packet spam
|
|
|
|
- Brak per-socket rate limit.
|
|
- Brak anty-spam dla `match.input`, `ping`, `queue.join`.
|
|
- `JSON.parse` i walidacja wykonywane dla kazdej ramki bez budget guard.
|
|
|
|
## 3.5 Heartbeat i stale sockets
|
|
|
|
- Brak WS-level ping/pong po stronie serwera (`ws.ping`).
|
|
- Heartbeat app-level (`ping`) tylko podczas meczu.
|
|
- Disconnect detection oparta o `lastSeenAt` (input lub ping).
|
|
|
|
## 3.6 Reconnect handling i ghost players
|
|
|
|
- Reconnect wymaga `matchId` hint w `hello`.
|
|
- Front usuwa `pp1v1.matchId` juz przy load strony.
|
|
- Browser refresh traci hint reconnectu.
|
|
- Efekt: mozliwy ghost player i draw timeout zamiast poprawnego resume.
|
|
|
|
## 3.7 Memory leak i dangling listeners
|
|
|
|
Server:
|
|
|
|
- `connections`, `userSockets`, `activeMatches` maja cleanup w typowych pathach.
|
|
- Brak graceful shutdown hooks (`SIGTERM`) moze zostawic stale keys i niesfinalizowane mecze.
|
|
|
|
Client:
|
|
|
|
- `setInterval` input loop zyje stale (celowo), ale wysyla tylko przy zmianie.
|
|
- Timery maja cleanup w return flow; brak twardego central cleanup managera, ale nie widac twardego leak path krytycznego.
|
|
|
|
---
|
|
|
|
# 4. GAME STATE
|
|
|
|
## 4.1 Gdzie przechowywany jest state
|
|
|
|
- Runtime authoritative state w `Match.state` (Node memory).
|
|
- Reconnect snapshoty w Redis (`match:{matchId}`) co `redisSnapshotMs`.
|
|
- Optional tick persistence do MySQL (`match_ticks`).
|
|
|
|
## 4.2 Authoritative model
|
|
|
|
- Ball/score/sets liczone po stronie serwera.
|
|
- Klient wysyla tylko input intent (`move`, `targetY`).
|
|
- Physics po stronie serwera.
|
|
|
|
## 4.3 Deterministic game loop
|
|
|
|
- Tick jest semi-deterministyczny:
|
|
- `dt` zalezny od czasu sciany i jitter scheduler
|
|
- `resetBall` uzywa `Math.random()` przy serwisie
|
|
- zatem brak strict determinism/replay determinism
|
|
|
|
## 4.4 Tick correctness
|
|
|
|
- Global single scheduler dla wszystkich matchy per worker.
|
|
- `dt` clamp 1ms..50ms ogranicza skoki.
|
|
- Przy duzym obciazeniu wszystkie mecze dziela jeden event loop worker.
|
|
|
|
## 4.5 Score i physics desync risk
|
|
|
|
- Score authoritative server-side, ale render client-side interpolowany predykcja.
|
|
- Desync UX mozliwy przy lag/jitter, logic desync final score mniej prawdopodobny.
|
|
|
|
## 4.6 Reconnect odzyskiwanie state
|
|
|
|
- dziala gdy klient poda prawidlowy `matchId` i trafi logicznie w reconnect path.
|
|
- refresh browsera jest problematyczny przez czyszczenie localStorage na starcie.
|
|
|
|
## 4.7 Miejsca powodujace instant draw / duplicate finish / phantom score
|
|
|
|
Krytyczne:
|
|
|
|
- `disconnect_timeout_{side}` konczy mecz jako draw (`winnerSide=null`) nawet gdy tylko jedna strona timeout.
|
|
- To umozliwia exploit: przegrywajacy disconnectuje i wymusza remis/refund.
|
|
|
|
Wysokie:
|
|
|
|
- Rozlaczenie + brak reconnect hint -> utrata kontroli paddle -> timeout draw.
|
|
- `_end()` ma guard `_ended`, wiec duplicate finish jest ograniczony.
|
|
|
|
Medium:
|
|
|
|
- MySQL `endMatch` i `processMatchResult` sa oddzielnymi operacjami; partial persistence mozliwa przy awariach miedzy krokami.
|
|
|
|
---
|
|
|
|
# 5. SERVER AUTHORITIVE ANALYSIS
|
|
|
|
## 5.1 Co jest server-authoritative
|
|
|
|
- Physics ball/paddle constraints
|
|
- Score/sets/end reason
|
|
- Match lifecycle state
|
|
- Final payload `match.end`
|
|
|
|
## 5.2 Co jest client-influenced
|
|
|
|
- Input intent frequency i pattern
|
|
- App-level ping values (`rtt` przesylane przez klienta)
|
|
- Queue join/leave cadence
|
|
|
|
## 5.3 Gdzie klient moze oszukiwac
|
|
|
|
- Nie moze bezposrednio ustawic score.
|
|
- Moze spamowac inputy dla DoS i unfair resource usage.
|
|
- Moze manipulowac reconnect pattern, by wymuszac draw przez timeout.
|
|
- Moze wysylac sztuczne `rtt` (informacyjne, niekrytyczne logicznie).
|
|
|
|
## 5.4 Czy klient moze manipulowac:
|
|
|
|
- wynikiem: bezposrednio nie, posrednio tak przez disconnect-draw exploit.
|
|
- pozycja: serwer clampuje i limituje velocity, wiec teleport cheating ograniczony.
|
|
- tickami: bezposrednio nie.
|
|
- eventami: moze floodowac i probowac replayowac legalne eventy.
|
|
|
|
## 5.5 Lista potencjalnych exploitow
|
|
|
|
- Intentional timeout draw exploit (ekonomia + rank integrity).
|
|
- Multi-tab/session race (duplicate_session tylko na active socket, nie na stale states).
|
|
- Reconnect hijack w oknie skradzionego ticketu (60s) bez nonce/one-time use.
|
|
- Flood `match.input` / `queue.join` / `ping`.
|
|
|
|
---
|
|
|
|
# 6. RECONNECT / DISCONNECT
|
|
|
|
## 6.1 Disconnect flow (server)
|
|
|
|
- `close` event:
|
|
- usuwa mapowania user socket
|
|
- user poza meczem -> leaveQueue
|
|
- user w meczu -> `onDisconnect` (chyba ze intentional leave)
|
|
|
|
- Dodatkowo safety net:
|
|
- brak input/ping przez `disconnectStatusMs` -> opponent status disconnected
|
|
- timeout `disconnectForfeitMs` -> end reason disconnect timeout
|
|
|
|
## 6.2 Reconnect flow
|
|
|
|
- klient wysyla `hello` z opcjonalnym `matchId`.
|
|
- server probuje local match reconnect.
|
|
- albo pyta Redis o owner workera i forwarduje `match.reconnect`.
|
|
|
|
## 6.3 Browser refresh i network interruption
|
|
|
|
Krytyczne:
|
|
|
|
- Front usuwa localStorage matchId przy init.
|
|
- Refresh usuwa hint niezbedny do `match.reconnected`.
|
|
- User po refresh zwykle nie dostaje legalnego session.matchId server-side.
|
|
|
|
Skutek:
|
|
|
|
- gracz moze nie moc wysylac inputow po refresh.
|
|
- mecz konczy sie timeout draw zamiast poprawnego resume.
|
|
|
|
## 6.4 Packet loss i websocket reconnect
|
|
|
|
- Klient probuje reconnect 5 razy (rosnacy delay).
|
|
- Brak explicit exponential jitter strategy per infra signal.
|
|
- Brak server-side session token dedicated for robust resume niezalezny od localStorage.
|
|
|
|
## 6.5 Ghost sessions i orphan matches
|
|
|
|
- Ghost sessions: mozliwe przez stale worker keys i crash bez cleanup.
|
|
- Orphan matches: mozliwe przy crash worker (brak graceful drain).
|
|
- Redis snapshot pomaga, ale ownership mapping liveness nie jest twardo gwarantowany.
|
|
|
|
## 6.6 Czy gracze moga przegrywac przez lag
|
|
|
|
- Tak, i nawet remisowac przez timeout przy chwilowym packet-loss > window.
|
|
- Disconnect status i timeout sa relatywnie agresywne dla niestabilnych sieci.
|
|
|
|
## 6.7 Edge cases (komplet)
|
|
|
|
- refresh strony w trakcie seta
|
|
- worker crash w trakcie meczu
|
|
- stale ownership key po crash
|
|
- `match.leave` frame utracony przy natychmiastowym close
|
|
- reconnect na innym workerze bez matchId hint
|
|
- oba sockety alive, ale brak input+ping -> false disconnect
|
|
- opoznione IPC message po zakonczeniu meczu
|
|
|
|
---
|
|
|
|
# 7. SYSTEM PLAYONS / WALLET
|
|
|
|
## 7.1 Aktualny model economy (as-is)
|
|
|
|
- Brak widocznego debit stake przy starcie meczu.
|
|
- Na koncu meczu wykonywane sa credit operations do `user_stats.balance`.
|
|
- Winner/loser rewards stale, hardcoded.
|
|
- Draw daje refund (tez hardcoded).
|
|
|
|
## 7.2 Ledger i atomic operations
|
|
|
|
- Jest tabela `transactions` i wpisy transakcji.
|
|
- Node direct path ma transakcje DB (`beginTransaction/commit`) i idempotency `match_rewards_log`.
|
|
- PHP fallback ma osobny flow `rewards_jobs` + inline processing i tez transakcje.
|
|
|
|
## 7.3 Krytyczny problem ekonomii
|
|
|
|
- Rozjazd reward constants:
|
|
- Node direct winner = 0.80
|
|
- PHP fallback winner = 1.00
|
|
- loser 0.20, draw 1.00
|
|
|
|
To oznacza niespojnosc finansowa zalezna od sciezki runtime.
|
|
|
|
## 7.4 Mozliwe exploity economy
|
|
|
|
Krytyczne:
|
|
|
|
- Disconnect timeout draw exploit:
|
|
- przy jednostronnym timeout mecz konczy sie draw
|
|
- to pozwala uniknac porazki i potencjalnie wymusic refund flow
|
|
|
|
- Brak stake debit before match:
|
|
- system jest praktycznie reward-only crediting
|
|
- mozliwa inflacja salda nawet przy przegranej (loser +0.20)
|
|
|
|
Wysokie:
|
|
|
|
- Dwa niezalezne settlement pathy (Node/PHP/cron) moga tworzyc rozjazdy operacyjne.
|
|
- DDL w runtime moze destabilizowac settlement pod obciazeniem.
|
|
|
|
## 7.5 Double spend / duplicate payout / rollback
|
|
|
|
- Node direct: idempotency przez `match_rewards_log` (dobry kierunek).
|
|
- PHP fallback: idempotency przez `rewards_jobs` unique match_key.
|
|
- Globalnie: trzy miejsca logiki reward (Node direct, PHP endpoint inline, CRON worker) zwiekszaja blast radius niespojnosci.
|
|
|
|
## 7.6 Floaty i rounding
|
|
|
|
- DB amounty sa DECIMAL(12,2) (dobrze).
|
|
- W payload/UI wystepuja float casty, ale glowna ksiega jest decimal DB.
|
|
- Rounding risk medium/low, glowny problem to logika stawek, nie precision.
|
|
|
|
---
|
|
|
|
# 8. REDIS ANALYSIS
|
|
|
|
## 8.1 Uzycie Redis
|
|
|
|
- Queue ZSET
|
|
- Queue lock string key
|
|
- Match snapshot string key JSON
|
|
- User->worker mapping
|
|
- Match->worker mapping
|
|
- IPC Pub/Sub channels per worker
|
|
|
|
## 8.2 Key structure
|
|
|
|
- `pp:1v1:queue:zset`
|
|
- `pp:1v1:queue:lock`
|
|
- `pp:1v1:match:{matchId}`
|
|
- `pp:1v1:ws:w:{userId}`
|
|
- `pp:1v1:match:w:{matchId}`
|
|
- `pp:1v1:ipc:w{workerId}`
|
|
|
|
## 8.3 TTL strategy
|
|
|
|
- user worker mapping: EX 7200
|
|
- match worker mapping: EX 14400
|
|
- snapshot match: zwykle 30 min, final snapshot 5 min
|
|
- queue entries: brak TTL (usuniecie explicit)
|
|
|
|
## 8.4 Stale keys i memory growth
|
|
|
|
- stale ownership keys po crash do TTL expiry.
|
|
- brak okresowego refresh heartbeat dla ownership (moze wygasnac przy dlugich sesjach).
|
|
- snapshoty maja TTL, wiec growth ograniczony czasowo.
|
|
|
|
## 8.5 Locks/pubsub/distributed sync
|
|
|
|
- Lock nie jest fenced i ma krotki TTL.
|
|
- Unlock jest best-effort (`get` + `del`), bez Lua atomic compare-delete.
|
|
- Pub/Sub daje co najwyzej at-most-once semantics.
|
|
- Brak durable queue dla IPC events.
|
|
|
|
## 8.6 Bottlenecks i scaling risks
|
|
|
|
- queue scan O(n)
|
|
- global lock contention
|
|
- cross-worker routing wymaga extra Redis operations per remote input/event
|
|
- przy duzym cross-worker mix moze byc Redis CPU/network bottleneck
|
|
|
|
---
|
|
|
|
# 9. DATABASE ANALYSIS
|
|
|
|
## 9.1 Transaction safety
|
|
|
|
- Node direct settlement: transakcyjny block i rollback (dobrze).
|
|
- PHP settlement: rowniez transakcja dla glownej logiki.
|
|
- `endMatch` update i final settlement sa rozdzielone (partial state possible).
|
|
|
|
## 9.2 Consistency i duplicate writes
|
|
|
|
- `INSERT IGNORE` + unique keys ograniczaja duplikaty.
|
|
- Rozne pathy rewardow moga miec inna semantyke payout.
|
|
- Brak jednego canonical write-service dla economy.
|
|
|
|
## 9.3 Indeksy i query performance
|
|
|
|
Pozytywne:
|
|
|
|
- `transactions` ma `(user_id, created_at)`.
|
|
- `match_results` ma unique `(discipline, mode, match_key)` i index winner/loser.
|
|
- `rewards_jobs` ma unique `(discipline, mode, match_key)` i index `(status, created_at)`.
|
|
|
|
Ryzyka:
|
|
|
|
- DDL wykonywany runtime w request path i settlement path.
|
|
- Optional `match_ticks` moze bardzo zwiekszac write volume.
|
|
|
|
## 9.4 Rollback safety
|
|
|
|
- rollback obecny przy exceptions.
|
|
- brak external saga compensation gdy czesc flow zakonczy sie po commit a przed broadcast/cleanup.
|
|
|
|
## 9.5 Query concurrency
|
|
|
|
- locki i contention potencjalne przy masowym settlement.
|
|
- connectionLimit default 20 na worker przy PM2 cluster moze latwo rozmnozyc laczne polaczenia do MySQL.
|
|
|
|
---
|
|
|
|
# 10. SECURITY ANALYSIS
|
|
|
|
## 10.1 WebSocket abuse
|
|
|
|
- Brak rate limiting i quotas per socket/user/IP.
|
|
- `maxPayload` 16KB jest, ale to nie zabezpiecza przed high-rate spam.
|
|
|
|
## 10.2 Replay attacks
|
|
|
|
- Ticket ma `exp` 60s i HMAC, ale brak nonce one-time store.
|
|
- Replay w oknie TTL jest mozliwy, ograniczony przez duplicate active session check.
|
|
|
|
## 10.3 Forged events
|
|
|
|
- Bez valid ticketu eventy sa odrzucane (`not_authenticated`).
|
|
- Po uwierzytelnieniu brak granular ACL na event frequency/shape poza podstawowa walidacja.
|
|
|
|
## 10.4 Session hijacking / reconnect hijacking
|
|
|
|
- Kradziez aktywnej sesji PHP lub ticketu umozliwia przejecie wejscia do WS w oknie TTL.
|
|
- Brak binding ticketu do IP/UA/fingerprint.
|
|
|
|
## 10.5 Reconnect hijacking
|
|
|
|
- Resume oparty o `matchId` i ownership keys.
|
|
- Brak dedykowanego signed reconnect tokena z rotacja.
|
|
|
|
## 10.6 Fake matches / fake payouts
|
|
|
|
- Rewards endpoint HMAC signed i ma timestamp skew check (dobrze).
|
|
- Brak nonce anti-replay w naglowkach HMAC, replay w oknie czasu blokowany glownie przez idempotency DB kluczy.
|
|
|
|
## 10.7 Redis abuse i DoS vectors
|
|
|
|
- Queue spam (`queue.join`) i input spam.
|
|
- Cross-worker remote input path generuje dodatkowe Redis obciazenie.
|
|
- Global lock + queue full scan to latwy target latency DoS.
|
|
|
|
## 10.8 Krytyczne dodatkowe ryzyko
|
|
|
|
- `session_bootstrap.php` zawiera hardcoded DB credentials (`root` + haslo) w kodzie.
|
|
- To jest security smell wysokiego ryzyka operacyjnego i audytowego.
|
|
|
|
---
|
|
|
|
# 11. PERFORMANCE ANALYSIS
|
|
|
|
## 11.1 CPU bottlenecks
|
|
|
|
- Matchmaking O(n) scan queue.
|
|
- JSON parse/stringify wysokiej czestotliwosci.
|
|
- Tick loop na pojedynczym event loop per worker dla wszystkich meczy workera.
|
|
|
|
## 11.2 Redis bottlenecks
|
|
|
|
- global lock queue
|
|
- zRange full
|
|
- remote routing extra calls (`getMatchWorker`/`ipcSend`)
|
|
|
|
## 11.3 Memory bottlenecks
|
|
|
|
- `activeMatches` i state obiektow rosna liniowo z liczba aktywnych meczy per worker.
|
|
- Snapshot JSON i state allocations per tick/persist.
|
|
|
|
## 11.4 WebSocket scaling i throughput
|
|
|
|
Przyblizenie (tylko `match.state`):
|
|
|
|
- Mecz: 30 tick/s * 2 klientow = 60 msg/s
|
|
- 1k graczy (~500 meczy): ~30k msg/s
|
|
- 10k graczy (~5k meczy): ~300k msg/s
|
|
- 100k graczy (~50k meczy): ~3M msg/s
|
|
|
|
Do tego input messages i ping oraz IPC overhead.
|
|
|
|
## 11.5 Tick loop cost i GC pressure
|
|
|
|
- Kazdy tick tworzy payloady JSON i serializacje.
|
|
- Brak pooling/zero-copy strategii.
|
|
- Potencjalnie duzy GC pressure przy bardzo duzym concurrency.
|
|
|
|
## 11.6 Czy architektura wytrzyma skale
|
|
|
|
1k graczy:
|
|
- Tak, przy dobrej infrastrukturze i monitoringu.
|
|
|
|
10k graczy:
|
|
- Ryzykowne bez zmian matchmaking i write-path.
|
|
- Mozliwe bottlenecks Redis i MySQL.
|
|
|
|
100k graczy:
|
|
- Obecna architektura nie jest gotowa.
|
|
- Niezbedne redesign matchmaking i transport efficiency.
|
|
|
|
Kilkaset tysiecy:
|
|
- Bez istotnej przebudowy distributed model i economy pipeline: nie.
|
|
|
|
---
|
|
|
|
# 12. CODE QUALITY ANALYSIS
|
|
|
|
## 12.1 Spaghetti dependencies / duplicate logic
|
|
|
|
- Economy logic jest zduplikowana i rozjechana:
|
|
- Node `processMatchResult`
|
|
- PHP rewards endpoint inline
|
|
- CRON worker rewards
|
|
|
|
- DDL obecny w wielu runtime pathach.
|
|
|
|
## 12.2 Anti-patterns
|
|
|
|
- Runtime schema migrations (CREATE/ALTER) w request handlers.
|
|
- In-memory Redis fallback w architekturze deklarowanej jako cluster distributed.
|
|
- Global queue scan lock pattern.
|
|
|
|
## 12.3 Unsafe async / unhandled promises
|
|
|
|
- Sporo fire-and-forget `void` calli (celowe), ale bez centralnego telemetry/compensation.
|
|
- Brak timeout/circuit breaker w `fetch` fallback rewards.
|
|
|
|
## 12.4 Missing validation
|
|
|
|
- Brak strict rate limiting eventow.
|
|
- `status.php` participant guard zalezy od payload completeness.
|
|
|
|
## 12.5 Missing cleanup
|
|
|
|
- Brak graceful shutdown hooks dla cleanup ownership keys i open matches.
|
|
|
|
## 12.6 Missing tests
|
|
|
|
- Brak test suite Node server (physics, reconnect, reward idempotency, queue races).
|
|
|
|
---
|
|
|
|
# 13. PRODUCTION READINESS SCORE
|
|
|
|
## 13.1 Ocena modulow (1-10)
|
|
|
|
- Core gameplay physics authority: 7/10
|
|
- WebSocket transport i event model: 6/10
|
|
- Matchmaking scalability/concurrency: 3/10
|
|
- Reconnect/disconnect resilience: 4/10
|
|
- Redis distributed coordination: 4/10
|
|
- Economy/playons settlement consistency: 3/10
|
|
- DB safety i idempotency foundations: 6/10
|
|
- Security hardening (abuse/replay/rate limit): 4/10
|
|
- Observability/operability: 4/10
|
|
- Testability/quality gates: 2/10
|
|
|
|
Global production readiness (dla duzej skali PvP): 4/10
|
|
|
|
## 13.2 Krytyczne bledy (Critical)
|
|
|
|
- C1: Disconnect timeout jednostronny konczy mecz jako draw (exploit fairness + economy).
|
|
- C2: Niespojne reward constants Node direct vs PHP fallback.
|
|
- C3: Brak stake debit i reward-only economy (inflation exploit vector).
|
|
- C4: Matchmaking O(n) full queue scan + global lock nie skaluje do high CCU.
|
|
- C5: Reconnect po browser refresh jest niestabilny przez usuwanie `pp1v1.matchId` na start.
|
|
|
|
## 13.3 High priority
|
|
|
|
- H1: Stale ownership keys i false-positive alive dla remote worker.
|
|
- H2: Brak event rate limiting i anti-spam.
|
|
- H3: Runtime DDL w settlement/request paths.
|
|
- H4: Brak graceful shutdown i recovery strategy dla aktywnych meczy.
|
|
- H5: Brak timeout/retry policy/circuit breaker dla fallback rewards fetch.
|
|
|
|
## 13.4 Medium priority
|
|
|
|
- M1: Brak deterministic replay capability (debug/anti-cheat forensic limitation).
|
|
- M2: Brak dedup/order handling dla `seq` w `match.input`.
|
|
- M3: Prosta klasyfikacja ping quality bez hysteresis.
|
|
- M4: Niewystarczajaca separacja warstw economy od gameplay orchestratora.
|
|
|
|
## 13.5 Low priority
|
|
|
|
- L1: Uporzadkowanie nieuzywanych helperow (`playerKey` etc.).
|
|
- L2: Ujednolicenie naming/reason messages.
|
|
- L3: Drobne UX niespjnosci statusow reconnect.
|
|
|
|
---
|
|
|
|
# 14. FINAL REFACTOR ROADMAP (kolejnosc dzialan)
|
|
|
|
## 14.1 Etap 0 - Immediate Hotfix Safety (najpierw)
|
|
|
|
- Ujednolicic semantyke `disconnect_timeout_*` (jednostronny timeout nie moze dawac remisu z refund policy).
|
|
- Ujednolicic payout constants i settlement source-of-truth.
|
|
- Zablokowac ekonomiczne rozjazdy miedzy Node/PHP/CRON.
|
|
- Naprawic reconnect po refresh (trwale i bezpieczne resume identity).
|
|
|
|
## 14.2 Etap 1 - Economy Integrity Core
|
|
|
|
- Jedna canonical sciezka ledger/settlement.
|
|
- Twarda idempotency warstwa i audit trail.
|
|
- Stake lifecycle end-to-end: reserve -> settle/refund -> journal.
|
|
- Ograniczyc runtime DDL do migracji deploymentowych.
|
|
|
|
## 14.3 Etap 2 - Matchmaking i Distributed Correctness
|
|
|
|
- Przebudowa matchmaking (atomowy dequeue bez full-scan).
|
|
- Sharding/bucketing queue.
|
|
- Worker/session heartbeat z twardym liveness, nie tylko key presence.
|
|
- Harden locks (atomic compare-delete / script).
|
|
|
|
## 14.4 Etap 3 - WebSocket Hardening i Anti-Cheat
|
|
|
|
- Rate limits per event/user/IP.
|
|
- Abuse budgets i temporary bans.
|
|
- Seq/order validation pipeline.
|
|
- Strong reconnect tokens i sesja resume security.
|
|
|
|
## 14.5 Etap 4 - Performance Scaling
|
|
|
|
- Ograniczenie write pressure MySQL (batch/coalesce, optional ticks policy).
|
|
- Optymalizacja serialization/state broadcast.
|
|
- Capacity tests 1k/10k/100k z SLO gates.
|
|
|
|
## 14.6 Etap 5 - Operability i QA
|
|
|
|
- Metrics/tracing/alerts (queue depth, tick lag, reconnect success, reward latency).
|
|
- Testy automatyczne (unit + integration + load + chaos disconnect).
|
|
- Runbook incident response.
|
|
|
|
## 14.7 Moduly najbardziej niebezpieczne
|
|
|
|
- `node-server/src/matchmaking.js`
|
|
- `node-server/src/index.js` (disconnect/reconnect lifecycle)
|
|
- `node-server/src/mysqlWriter.js` + `api/matches/ping-pong/1v1/index.php` (economy divergence)
|
|
|
|
## 14.8 Co przepisac calkowicie vs poprawic
|
|
|
|
Przepisac (high confidence):
|
|
|
|
- matchmaking engine (algorytm + distributed lock semantics)
|
|
- settlement orchestration (single source ledger flow)
|
|
|
|
Mocno przebudowac:
|
|
|
|
- reconnect/session restore protocol
|
|
- distributed ownership liveness model
|
|
|
|
Poprawic inkrementalnie:
|
|
|
|
- physics core (dziala relatywnie poprawnie)
|
|
- UI interpolation i ping UX
|
|
- health/monitoring endpointy
|
|
|
|
---
|
|
|
|
## Konkluzja
|
|
|
|
System ma solidny fundament server-authoritative gameplay dla 1v1, ale obecnie nie jest production-ready dla wysokiej skali i ekonomii stake/playons o wysokiej integralnosci. Najpowazniejsze ryzyka dotycza nie fizyki gry, tylko consistency economy, reconnect correctness i distributed matchmaking semantics pod obciazeniem. |