Files
Hermes Agent 101a4b60a9 docs(sec): SEC-16 — безвозвратная утечка ренты PDA-ставки с кошелька игрока
Найдено при приёмке боевого домена smart-updown.binom.pw (симптом «SOL не обновляется»).

- SEC-16: при create_bet ренту PDA-аккаунта ставки (1 559 040 lamports = (96+32+128)*6960)
  платит БЕТТОР, а не шлюз; баланс шлюза-админа при этом не меняется (замер whose-fee.cjs).
  Аккаунт не закрывается никогда (в контракте 9ALs четыре инструкции, ни одной close = bettor)
  → рента теряется безвозвратно. В цепи 75 аккаунтов = 0.116928 SOL заперто, растёт линейно.
  Вердикт: на тестах терпимо (маскируется краном, 5 SOL = ~3207 раундов), перед mainnet — блокер.
- TASKS.md: фича «если цена не изменилась — подождать ещё 15 с» (НЕ доделана), заметка про
  боевой домен на общем TS-шлюзе и лаг крана 8–13 с.
- tools: доказательные скрипты (whose-fee, bet-pda-rent, faucet-latency, sol-balance-cycle,
  settle-math-test, backend-full-test) + README с питфоллами замера.
- k8s/smart-updown-api-ingress.yaml: /api,/ws вынесены вне helm (релиз игры их сносил).
2026-09-15 05:57:34 +03:00

121 lines
8.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Инструменты проверки игрового контура
Скрипты для **живой** приёмки механик ставок (не заменяют `npm test` в репозитории релейера,
а проверяют боевой стенд).
## `exploit-decisive.cjs` — решающая проверка окна закрытия
Проверяет главное: **цена закрытия берётся из истории на момент экспирации, а не в момент клика.**
Как запускать (внутри пода шлюза — там есть `node_modules` и доступ к RPC):
```bash
P=$(kubectl -n game get pod -l app.kubernetes.io/instance=testfront-gateway -o jsonpath='{.items[0].metadata.name}')
kubectl -n game cp tools/exploit-decisive.cjs "$P:/app/data/probe/exploit-decisive.cjs" -c gateway
kubectl -n game exec "$P" -c gateway -- sh -c "cd /app && node /app/data/probe/exploit-decisive.cjs 300"
```
### Питфоллы (каждый пойман вживую)
- **Нельзя просто сравнивать `exitPrice` с текущей ценой.** Цена часто стоит на месте, и сравнение
вырождается в «совпало» — ложный успех. Скрипт ждёт, пока цена на экспирации и текущая
**разойдутся**, и только тогда закрывает. Первый прогон без этого дал ложное «совпало» дважды.
- **Копировать только в `/app/data/`**, не в `/app` (`Permission denied`, под не root).
- **Запускать с `cd /app`**, иначе `Cannot find module '@solana/web3.js'` (из `/tmp`/`/app/data`
без cwd `node_modules` не виден).
- Ставка при `exit == entry`: с 2026-09-14 (`SEC-9`) **возвращает залог** статусом `3` и
финализируется — залипаний больше нет. *Открытый вопрос:* хочется вместо немедленного возврата
**подождать ещё 15 с** и взять цену заново (см. `TASKS.md`, «ждать, если цена не изменилась»).
Читает `bet_time` / `expire_time` прямо из аккаунта Bet (сырые байты) — off-chain данными не
верим. Раскладка аккаунта (Anchor, после 8-байтового дискриминатора): `amount u64 @8`,
`bet_time i64 @16`, `expire_time i64 @24`, `entry_price u64 @32`, `exit_price u64 @40`,
`side u64 @48`, `status u64 @56`, `bettor Pubkey @64`.
## `price-history-probe.cjs` — разбор истории цены
Смотрит, что реально накопилось в `binance-market` (Postgres `192.168.76.182:5434`, БД
`binance_market`) вокруг конкретной ставки: сделки в окне, цена на входе, цена на экспирации,
и что было бы при «ждать выгодной цены».
```bash
node tools/price-history-probe.cjs <programId> <betId>
```
⚠️ **Символ в БД — в нижнем регистре** (`solusdt`, не `SOLUSDT`). Запрос с верхним регистром
вернёт пусто, и это легко принять за «данных нет».
## `whose-fee.cjs` — с ЧЬЕГО кошелька уходит SOL за раунд (доказательство `SEC-16`)
Мерит SOL **игрока** в трёх точках каждого раунда (до ставки → после ставки → после расчёта)
и SOL **шлюза-админа**. Замер: игрок `5 000 000 000 → 4 998 440 960` (−**1 559 040** lamports),
шлюз не меняется — значит ренту PDA-аккаунта ставки платит игрок.
```bash
node tools/whose-fee.cjs
```
## `bet-pda-rent.cjs` — сколько ренты заперто в аккаунтах ставок
Обходит `getProgramAccounts` (фильтр `dataSize:96`) и показывает, что у каждой ставки PDA
держит ровно **1 559 040** lamports. Аккаунт не закрывается никогда (`close_bet` не делает
`close = bettor`) → рента теряется безвозвратно. Второе доказательство `SEC-16`.
```bash
node tools/bet-pda-rent.cjs
```
## `faucet-latency.cjs` / `sol-balance-cycle.cjs` — почему кажется, что «SOL не обновился»
`GET /faucet/{addr}` отвечает **мгновенно**, но `getBalance` **на цепи** показывает `0` ещё
**8–13 с** (замеры: 13309 / 13345 / 13345 мс). Игра читает баланс не через `/api/balance`, а
через свой nginx-прокси `/solana-rpc/getBalance` → прямо с цепи → попадает в окно лага.
Симптом «налил, а SOL не обновился» — это лаг зачисления, а не отставание шлюза.
```bash
node tools/faucet-latency.cjs # лаг крана: цепь против отчёта шлюза
node tools/sol-balance-cycle.cjs # баланс после крана: сразу 0, через ~20 с 5 SOL
```
## `settle-math-test.cjs` — математика расчёта (правильный способ замера)
⚠️ Шлюз **кастодиальный**, поэтому дельту надо считать **от момента после lock** (фантики ушли
в vault при ставке), а НЕ от старта раунда — иначе в дельту попадают и ставка, и выплата, и
«математика не сходится» ложно. Замерено: выигрыш `+130 000 000` (1.3×), проигрыш `0`,
ничья `+100 000 000` (возврат). Считать «от старта» = получить ложный провал (проверено живьём).
```bash
node tools/settle-math-test.cjs
```
## `backend-full-test.cjs` — контракт ручек и коды ошибок шлюза
Приёмка API живьём: `/state`, `/price`, `/wallet`, `/faucet`, `/balance`, `/bet`, идемпотентность
крана, `400` на кривые данные, `409` на вторую открытую ставку, `404` на снятые ручки
(`/close`, `/bet/{id}`), WS-события `bet_open`/`bet_closed`. Полный прогон с расчётом на цепи.
```bash
node tools/backend-full-test.cjs
```
⚠️ `/bet` требует **число** в `amountUnits` (строка → `400`) и `address` в теле.
⚠️ `getTokenAccountsByOwner` принимает **2–3** параметра: commitment кладётся **внутрь третьего**
(`cfg`), а не четвёртым — иначе `-32602 Expected from 2 to 3 parameters`.
## `k8s/smart-updown-api-ingress.yaml` — пути `/api`,`/ws` вне helm
⚠️ Чарт `fun-game-front2` рендерит **только** path `/`, поэтому любой `helm upgrade` игры
**сносит** `/api` и `/ws` с боевого домена (домен начинает отдавать HTML игры на `/api/*`).
Лечение — отдельный ingress `smart-updown-api` **вне helm**; front2-ingress остаётся `paths: [/]`.
```bash
ssh root@192.168.76.120 "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml; \
kubectl apply -f /root/smart-updown-api-ingress.yaml"
# проверка после каждого релиза игры:
kubectl -n game get ingress -o custom-columns='NAME:.metadata.name,HOST:.spec.rules[0].host,PATHS:.spec.rules[0].http.paths[*].path'
```
⚠️ НЕ править paths через `kubectl patch --type=json` с `remove` по индексам: при нескольких
путях индексы съезжают, остаётся висячий `/ws`, а последний path удалить нельзя
(`paths: Required value`). Надёжно — собрать нужный список целиком и `kubectl replace --force`.